Merge remote-tracking branch 'upstream/dev' into 170321_url

This commit is contained in:
Wenkai Yin 2017-03-24 14:56:55 +08:00
commit d83554013b
67 changed files with 1520 additions and 1023 deletions

View File

@ -101,7 +101,7 @@ script:
- docker ps
- ./tests/notarytest.sh
- go run tests/startuptest.go https://localhost/
- ./tests/startuptest.sh
- go run tests/userlogintest.go -name ${HARBOR_ADMIN} -passwd ${HARBOR_ADMIN_PASSWD}
# - sudo ./tests/testprepare.sh

886
Makefile
View File

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

51
docs/use_make.md Normal file
View File

@ -0,0 +1,51 @@
### Variables
Variable | Description
-------------------|-------------
BASEIMAGE | Container base image, default: photon
DEVFLAG | Build model flag, default: dev
COMPILETAG | Compile model flag, default: compile_normal (local golang build)
GOBUILDIMAGE | Golang image to compile harbor go source code.
CLARITYIMAGE | Clarity image that based on Node to compile UI.
NOTARYFLAG | Whether to enable notary in harbor, default:false
HTTPPROXY | Clarity proxy to build UI.
### Targets
Target | Description
--------------------|-------------
all | prepare env, compile binaries, build images and install images
prepare | prepare env
compile | compile ui and jobservice code
compile_ui | compile ui binary
compile_jobservice | compile jobservice binary
compile_clarity | compile clarity ui binary
compile_adminserver | compile admin server binary
build | build Harbor docker images (default: using build_photon)
build_photon | build Harbor docker images from Photon OS base image
install | compile binaries, build images, prepare specific version of compose file and startup Harbor instance
start | startup Harbor instance
down | shutdown Harbor instance
package_online | prepare online install package
package_offline | prepare offline install package
pushimage | push Harbor images to specific registry server
clean all | remove binary, Harbor images, specific version docker-compose file, specific version tag and online/offline install package
cleanbinary | remove ui and jobservice binary
cleanimage | remove Harbor images
cleandockercomposefile | remove specific version docker-compose
cleanversiontag | remove specific version tag
cleanpackage | remove online/offline install package
version | set harbor version
#### EXAMPLE:
#### Build and run harbor from source code.
make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=danieljt/harbor-clarity-base:0.8.4 NOTARYFLAG=true HTTPPROXY=http://proxy.vmware.com:3128
### Package offline installer
make package_offline GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=danieljt/harbor-clarity-base:0.8.4 NOTARYFLAG=true HTTPPROXY=http://proxy.vmware.com:3128
### Start harbor with notary
make -e NOTARYFLAG=true start
### Stop harbor with notary
make -e NOTARYFLAG=true down

23
docs/use_notary.md Normal file
View File

@ -0,0 +1,23 @@
### Setup
In harbor.cfg, make sure the attribute ```ui_url_protocol``` is set to ```https```, and the attributes ```ssl_cert``` and ```ssl_cert_key``` are pointed to valid certificates. For more information about generating https certificate please refer to: [Configuring HTTPS for Harbor](configure_https.md)
### Copy Root Certificate
Suppose the Harbor instance is hosted on a machine ```192.168.0.5```
If you are using a self-signed cetificate, make sure to copy the CA root cert to ```/etc/docker/certs.d/192.168.0.5/``` and ```~/.docker/tls/192.168.0.5/```
### Enable Docker Content Trust
It can be done via setting envrironment variables:
```
export DOCKER_CONTENT_TRUST=1
export DOCKER_CONTENT_TRUST_SERVER=https://192.168.0.5/notary
```
### Set alias for notary (optional)
Because by default the local directory for storing meta files for notary client is different from docker client. If you want to use notary client to manipulate the keys/meta files generated by Docker Content Trust, please set the alias to reduce the effort:
```
alias notary="notary -s https//192.168.0.5 -d ~/.docker/trust --tlscacert /
etc/docker/certs.d/192.168.0.5/ca.crt"
```

View File

@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIF3TCCA8WgAwIBAgIJANgnJg8tUB+HMA0GCSqGSIb3DQEBCwUAMIGEMQswCQYD
VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJUGFsbyBBbHRv
MRUwEwYDVQQKDAxWTXdhcmUsIEluYy4xDzANBgNVBAsMBkhhcmJvcjEkMCIGA1UE
AwwbU2VsZi1zaWduZWQgYnkgVk13YXJlLCBJbmMuMB4XDTE3MDMyNDA1MzE1N1oX
DTI3MDMyMjA1MzE1N1owgYQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9y
bmlhMRIwEAYDVQQHDAlQYWxvIEFsdG8xFTATBgNVBAoMDFZNd2FyZSwgSW5jLjEP
MA0GA1UECwwGSGFyYm9yMSQwIgYDVQQDDBtTZWxmLXNpZ25lZCBieSBWTXdhcmUs
IEluYy4wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQClgcA3XhXFgaBa
iK5G60ym0SB0P4KDyB0aKz1nQwf3svJdzUOLzom3zK8mUDXZ5b0Jnix5KrW6CONs
JsjPtZKRXVNkWhUh6362OUt2icmq3BLGqKQ9qTqi4R1NrPr4vug/TmBumxMB+JJI
UHRJgLox1dXUEsyxxv5yt/AKPa9nZruI2x8CzdKRVhsiR06B70OJZA8l2UuRv7v8
9biGGu4Haavt4CG0goPBXh7PpPNHcoQmgdMAHkawBmrf3qvn2nSrJzfbjsv6iQ9/
e3GRAmWmJVsDBvlxwtIJDXLvm3qUN/P/ul6w6zbueAXkAq5UcjIMdDLSnt690DWo
B7cO8FWKg4TqvuJ0+qb9Uwty+3x/mONiq9kwbFIKuLnjRJApPO1gevGexotiOyKp
ljJMkeabPCuClquqI+LxM+TEmDtxOfJ2OuhisOaAuW2qYl2ZdnaTaVz42kctobwj
+DnhvtwItE88mf8tYxDY+Kp+bITlcanmSPASw/YJXMrIbPynzMPCloe2TRSoImGC
8uQI6rLSyeUvkpCCxIDnfUTuhmSc2jseqTYyxXrf+qMVNNoTC2VMUwt/nxerjK1a
L000KIqk4h0GqUwuAE6I1CPLN9eQE9qlaeSxKPiScPG3M3mkKyIIAKUz3WjR7UnW
Aw1Z5fRH28ci8GfbxynTMuWlU/izqwIDAQABo1AwTjAdBgNVHQ4EFgQUZP1uZGYH
85c0RIrIJVr/RdC64YcwHwYDVR0jBBgwFoAUZP1uZGYH85c0RIrIJVr/RdC64Ycw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEACYIVf0U2kc849GlpvYCv
LDGjbdswjmjAxpZaKFCO3MjAEhDxd8QWb1uCN+asRKV146qU3UL40stjjWUpwx6P
YQ48zJi1N+Npc53NWoTQ8JxsmQtATTaIlAgYg1WC1oTg5WTPeNOAY/KuSiwPHIrX
yaCJdz0+c1xKRRE1m3m85amrtAJkIigL8WIPsKqnNprP11zLzaebMJNpGwq2lRsI
4Sm0SEdJNaOm3fQ8KuTElBAGEmJ3F34FeNajM+hIkd0RnG35nsJQgMQz36E5rMVd
P1Djk/wPfXJIk61lGJvS/Rl41c1d+XG8aFjhL0APdYHddB11IZ7+QNslEk11kiVI
nNjx5CfFuE6ZSq/TAVrco97TxqKdbMIMkRp/MKoTlxG4O5UlFGOniGvQT4g1A962
aobnVvxkIhZ5NbPc8PX18EdfpQcheubDZuQtZMmcdU7ilFI0pP9/bQ2EYKi2oPJv
4v6vtCYKU2et2KLJLFt7zUoY4zJGqJcW8BibP5kDkmAT+qxurH6T5X+M2QctdxU/
63L3sE/dH3saSAVNqB1hs+9pweEj6E+Uaj6Oyn9UDarri11y+esyVPdBEnHwCEsc
o3/KMSc7gXfixQi+WgRoD0DpR/bNatjgbq7KSGi9gZp/Aq+ltx5I49nbf4c+WZ9b
l7WOOMS8XTJr7KLDUXkAeic=
-----END CERTIFICATE-----

View File

@ -1,63 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFBDCCAuygAwIBAgIJAMbWdVJcKhXYMA0GCSqGSIb3DQEBCwUAMGwxCzAJBgNV
BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0G
A1UECgwGRG9ja2VyMScwJQYDVQQDDB5Ob3RhcnkgSW50ZXJtZWRpYXRlIFRlc3Rp
bmcgQ0EwHhcNMTcwMTIzMDYwMzM3WhcNMTkwMjEyMDYwMzM3WjBbMQswCQYDVQQG
EwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzANBgNV
BAoMBkRvY2tlcjEWMBQGA1UEAwwNbm90YXJ5LXNpZ25lcjCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBANhO8+K9xT6M9dQC90Hxs6bmTXWQzE5oV2kLeVKq
OjwAvGt6wBE2XJCAbTS3FORIOyoOVQDVCv2Pk2lZXGWqSrH8SY2umjRJIhPDiqN9
V5M/gcmMm2EUgwmp2l4bsDk1MQ6GSbud5kjYGZcp9uXxAVO8tfLVLQF7ohJYqiex
JN+fZkQyxTgSqrI7MKK1pUvGX/fa6EXzpKwxTQPJXiG/ZQW0Pn+gdrz+/Cf0PcVy
V/Ghc2RR+WjKzqqAiDUJoEtKm/xQVRcSPbagVLCe0KZr7VmtDWnHsUv9ZB9BRNlI
lRVDOhVDCCcMu/zEtcxuH8ja7fafi5xNt6vCBmHuCXQtTUsCAwEAAaOBuTCBtjAf
BgNVHSMEGDAWgBQjgpNYJjU9Ei7nadpOhHm59FPiKTAMBgNVHRMBAf8EAjAAMB0G
A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAOBgNVHQ8BAf8EBAMCBaAwNwYD
VR0RBDAwLoINbm90YXJ5LXNpZ25lcoIMbm90YXJ5c2lnbmVygglsb2NhbGhvc3SH
BAp1BI4wHQYDVR0OBBYEFLv4/22eN7pe8IzCbL+gKr2i/o6VMA0GCSqGSIb3DQEB
CwUAA4ICAQBzBcFgcwtr7oNP7WPyG64mRXHFs1qGCoDZO3D2dZPF/vUKnyPWI6+i
Ozu1Lmvd6QUQ5C0m91D6RidKKy3ENz2MgUo8NNj3QY3XzassiLnNOtpo1ed6U3BG
2w05gaLTTFywnpOgPy180U6f5uNSHGxY/fq9dN+8YR/MqGOht74q36x0swkPegG/
+0SLloKOJw1wBzZ4nCLmED08DWNnuNTAj5IIVjApzqZbTh4+z6H1lmN3b7XwmiWw
+y7Jx8k74h5JmqKQnV+3lN0DlCc1BCbtH2fbKOmAKeu4gMniw5FBo75wYrPIet+Z
E3G2Zg+T6fjTXAnLGT3S0RVn/CW1lLR6RgkoFgURRZoJyTWrg+1yu4ZOgEz+bot2
/hMAr/fjo+Dd6ReFrgGkpTyWYtPhYusori1W8KW138CVrJmSs6p2ss1Ixh8uIOaQ
iFmlX/ZXXbvkz3FGQS9LfBdESO3MGjiJTcnXE0DTnXf6RmdlUfNwxsZbIliFa0TQ
E/JjIJYQzWmtkJbUdC02GUMjUJAM7SxmP7tU9CmMmjUI28Nno0XtPN2WsAszaiLh
JYLJCi7rqaLo0oZuaXVIrgBpQ0qEC1XXS5sCQL+xvMSYvke/rhwIPItWt7Ww/9yj
QDIi1nzzX86lbKd095pNX4sUfFx6j4caR8iENgJDfWnqynAzj1Y21A==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIF1TCCA72gAwIBAgIJAMk2DFRLRSRRMA0GCSqGSIb3DQEBCwUAMF8xCzAJBgNV
BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0G
A1UECgwGRG9ja2VyMRowGAYDVQQDDBFOb3RhcnkgVGVzdGluZyBDQTAeFw0xNzAx
MjMwNjAzMzdaFw0yNzAxMjEwNjAzMzdaMGwxCzAJBgNVBAYTAlVTMQswCQYDVQQI
DAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGRG9ja2VyMScw
JQYDVQQDDB5Ob3RhcnkgSW50ZXJtZWRpYXRlIFRlc3RpbmcgQ0EwggIiMA0GCSqG
SIb3DQEBAQUAA4ICDwAwggIKAoICAQCu+ldASegXuhXrA7mnk4nybTEomHnV8zJ/
uU6+8bWIo+htD8zgiONuk1uEww0p/nWtIZqm7xpLsklMp0CWRA8EAeUnxfNJ37ks
7nZuJ+YDtw77fC0IUJSWqFbro75nPMyegMqajT7IDWfLeTrIlgUmDu/45AWdbE2w
BrRgejqkL1yeQPaldgr97g00swbTd7wzWn1o6025Frm0kDEIqMJlkB61cHiVGZNu
oeDBZcFiwa/Ek/keDG3Y2R6cDQzZa8aEZG9i3Cmo0nGviojr+06JxQ8IkVc5P72e
Fb/jgX/NvRaqeBnJrZoiPnuMoMag/ynGC9fuIAGz25fKOuGOf52x+swzQB2ZVtxA
BIgIZIbMTURKknqbl6LAh46onQUVF+3h9E9Te3a4Oh7SvSGLYfEbWprPKo1J3lI9
ApU19TBhKUrj7dsJT3gri7f71NC2RLraZbpK3d8PWKMc/q4ffoRCeW+TPjYreC/d
7LdykAwYB2AGyHCLHkkkJC86n6wAsk/TaoTgjflyyQ35FNikUYqNF/rVuc+0Oj5R
odPk8y2vB7VvPvWWlttcr7OMqVVAymQvDOTb+5T6EI/LdHejjDMMI5lt6rVUU+uq
kGMYGiHtWG5JqQdhUBpISYuF74cS5aVRmnhK6O2ylMpmlWYq4128SRv8EEAPNcN9
V/RrOF9RsQIDAQABo4GGMIGDMB8GA1UdIwQYMBaAFJZZtwJ5t4SBmVaTb+T5puH5
sQWkMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG
AQUFBwMCMA4GA1UdDwEB/wQEAwIBRjAdBgNVHQ4EFgQUI4KTWCY1PRIu52naToR5
ufRT4ikwDQYJKoZIhvcNAQELBQADggIBAI64zW1o24R8K7qsE8FO3UHJQdizR1RC
FvMDgXGDSYMUg4QkEvHYYOoFH1zMd1HNUuLDO231dtw23kshNY/kdKfdFJktT3Dz
50r/hl2090uZIOk9aLv7swG0voA6A8CI2qyXEXW9Le8xrnrJUU5T+3YDxseHokTT
XT9hLd1iSNH5gi3tOaJ4KNbHc2zhKtQSUZbxguapUIUXStiQLz06itQu3i1fLdMd
L3yRJID4aWU+Dmm5AQ6F3ticIpzFmJyAsTM2BMiTnlSJPK3LA2WYMBOVD6r9yo08
cEpi6Vo8pZdsnHWaIaIkO4UR7iBwmkT0h8HfNZ4uEoViiMsxqNVsQBfJR/9DzAXz
ctO6JtNJdNwn2zlv4NCIcV0AdncVf049uOtTBWIqRn1IHQ8d119lQAMXZZMSNKBI
lIYFCKMh95XI6mK6VFsFKs2wSDiSH4ZOqIwr4urmr1opLNJ5T5Ck18YwJafgCH4p
1BcgR06wuw5ckIuUyUwiakiGINZcrzUnAoRtEKsVi/PQAC+45veo8Lcvwnj5X0vg
PKudwiJivo7Umvj1xEVyVIy+22cyDk/yLTVI0sS2Kpuwd+PLE16C5+nPr8wKEWqL
ccotlod4ZDVb6vNU5VRUSu4bSYBry/FbftPNgAwfH8ufSddeJMjTQ+V69XrQZ5Ex
XJCKYD/1jYIB
MIIFdjCCA14CCQCeVwANSZmmiDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC
VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCVBhbG8gQWx0bzEVMBMG
A1UECgwMVk13YXJlLCBJbmMuMQ8wDQYDVQQLDAZIYXJib3IxJDAiBgNVBAMMG1Nl
bGYtc2lnbmVkIGJ5IFZNd2FyZSwgSW5jLjAeFw0xNzAzMjQwNTMyMDBaFw0yNzAz
MjIwNTMyMDBaMHUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIw
EAYDVQQHDAlQYWxvIEFsdG8xFTATBgNVBAoMDFZNd2FyZSwgSW5jLjEPMA0GA1UE
CwwGSGFyYm9yMRUwEwYDVQQDDAxub3RhcnlzaWduZXIwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQC6TV2RCoH8d1g6xFvDo4FL9v+pGLe5+bu9ryjTaLbN
dH/Cmf5/8WrmgJ3vG2Ksk796J7qsVddwvQkZn6NwDm2Tm+ETMCG85yEA3jl4Kr9R
XfWHYWEavv0vsq6M+bUSSq7VJAhgk4wfx6qJBnFX2qKpODeYLHaHxU1EnIXrStNf
IqR4Eu0Xre8jAkzrDdaFy/KnX4HGgNdz413CXzBCKEuu3VJj07ZvonnTzOgoLvh8
+PCoQ2M4OBPT9gHqUov1I8nWnrjc+HuM1BW3YIGCB5TV9x0Y7hjvkr4E38gbJURj
uDwg8jof4lMRmU/FHXFLt1ucGwNFUJdPwI7dyEKRA03Lr7htfP5sa9tmv3L93dKD
po1gW1LsfiM3Cur5jARM/hBA+eYJr12Laf9oL59r8JmweqF3zRSwGSY336XoR/Fv
/PAFs9vfKKWZp0uiRtuY9JZNRTF8trnfNf1957bND+DS2HWPmWkw4yK6CGa0s55X
adiDt4gDFvKjl68dBWZoHutY+cZy/hK1D5uqagcX1kzbr/Pzy1gsq9FBBwaTJqBu
YIAsSuzP+7NNZXoPd3rg13V93pbZr8eQN5VOQIBZK83xZEtHSJBEdUSuBOo3JS7j
/rjEnspRqOI4soFnx1vaK0TrRyzJ5KBOuGpW4u8/ZUdIq8KIE30Mj/XI/sgAPr5j
UQIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQBjqYBm/FRqyMH2hnHA0TMXY/WPufJ8
TX10daELCAYJCEETXmUt1i7dnFxdAZXTnHENHdNYiS4nGBfqMLmODtcAamcv6Dcl
JnyQPt3QlCDPKkcHgz3y4tvDDx6M5rFWYzN9QLiWAYrunIk1R4Jj7FODrM6/NODE
0Mz1czWfsmLfX/jF80SsxnY1DCLKGgo6/RID3xTp4eIMboxCfeH2/yDA+6YPyYbV
Si4ccwo9Foq0IYU8bimPNTyBQ0N+8ajcn328ql6aazmr894Ch5pWA3Qxaa98FcKS
zokBvmmCuvCJ9HOmxKWdFEhSRS9GWxn7wg78UIlLP/8RfUrsecBJHgyhWRA7Qs3K
keiG68Zrhn456IdMxjCZXgJ7gAAe77n4Cz8sFEHAvnAg9JLNEHuEBV5H1Hb7TzET
k0lPiEY78QjutOpqHsWiagqSjlGEMqKI9c8WxXHh9030T/6NnWkdXFo+4HaEZEpp
0JryASS53B5SwLIPrn0Y2/io/kRgbglGktPt6Ex0DwW3f96lcz3me34Nw+HOYYnz
b0cz7JqJZgFXfEnykic3IwZs7m7Xrl9B/vvaVub9Fb5LQ7rIzrO7VkoILov/G41B
Pd4/kagjXDTWd+UBMvZF6YGjr+TUZi5ooi7bvQ3X6N9WNYKW4a1DOokz9janStiL
MrTKyOEOBi0Aew==
-----END CERTIFICATE-----

View File

@ -1,28 +1,52 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA2E7z4r3FPoz11AL3QfGzpuZNdZDMTmhXaQt5Uqo6PAC8a3rA
ETZckIBtNLcU5Eg7Kg5VANUK/Y+TaVlcZapKsfxJja6aNEkiE8OKo31Xkz+ByYyb
YRSDCanaXhuwOTUxDoZJu53mSNgZlyn25fEBU7y18tUtAXuiEliqJ7Ek359mRDLF
OBKqsjsworWlS8Zf99roRfOkrDFNA8leIb9lBbQ+f6B2vP78J/Q9xXJX8aFzZFH5
aMrOqoCINQmgS0qb/FBVFxI9tqBUsJ7QpmvtWa0NacexS/1kH0FE2UiVFUM6FUMI
Jwy7/MS1zG4fyNrt9p+LnE23q8IGYe4JdC1NSwIDAQABAoIBAHykYhyRxYrZpv3Y
B6pUIHVX1+Ka4V98+IFrPynHNW9F7UzxmqNQc95AYq0xojQ4+v6s64ZjPMYHaaYW
/AsJKamN+sRNjEX8rko9LzIuE7yhp6QABbjXHPsAiPgZdF5CrFX2Q558yinHfFeC
sualDWK3JxEajaiBGU8BEGt2xAymuWACGblrM1aAEZa8B84TW3CzzcdyzAkn8P3e
piJCe+DWMc33441r0KlV5GruwF9ewXiWzZtXAOiP/0xEDICFdlFWbO39myMpxDdU
Y0uZ+zmn2G3gz2tz25thH0Wl7mDQ3AA0VlHurgPBBEekeZPQmjiKW+F4slCzXvuy
kW/urIECgYEA/LhY+OWlZVXzIEly7z1/cU9/WImqTs2uRKDeQHMwZrd7D9BXkJuQ
jPN+jZlMYBBrxoaCywbMrgB80Z3MgGHaSx9OIDEZmaxyuQv0zQJCMogysYkbCcaD
mHYnyAf7OXa708Z168WAisEhrwa/DXBn3/hPoBkrbMsuPF/J+tEP7lsCgYEA2x2g
86SitgPVeNV3iuZ6D/SV0QIbDWOYoST2GQn2LnfALIOrzpXRClOSQZ2pGtg9gYo1
owUyyOSv2Fke93p3ufHv3Gqvjl55lzBVV0siHkEXwHcol36DDGQcskVnXJqaL3IF
tiOisuJS9A7PW7gEi0miyGzzB/kh/IEWHKqLL9ECgYEAoBOFB+MuqMmQftsHWlLx
7qwUVdidb90IjZ/4J4rPFcESyimFzas8HIv/lWGM5yx/l/iL0F42N+FHLt9tMcTJ
qNvjeLChLp307RGNtm2/0JJEyf+2iLKdmGz/Nc0YbIWw46vJ9dXcXgeHdn4ndjPF
GDEI/rfysa7hUoy6O41BMhECgYBPJsLPgHdufLAOeD44pM0PGnFMERCoo4OtImbr
4JdXbdazvdTASYo7yriYj1VY5yhAtSZu/x+7RjDnXDo9d7XsK6NT4g4Mxb/yh3ks
kW1/tE/aLLEzGHZKcZeUJlISN57e6Ld7dh/9spf4pajuHuk1T6JH+GNKTAqk5hSQ
wmKJIQKBgCGBWGvJrCeT5X9oHdrlHj2YoKvIIG1eibagcjcKemD7sWzi7Q4P7JIo
xeX8K1WVxdBpo4/RiQcGFmwSmSUKwwr1dO00xtjxIl7ip4DU+WAM7CdmcOIOMbr4
rP9T/wy1ZBkERCIw2ElybTzB8yuOlNLuOMhUeU55xUMFNYYrWEp2
-----END RSA PRIVATE KEY-----
-----BEGIN PRIVATE KEY-----
MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC6TV2RCoH8d1g6
xFvDo4FL9v+pGLe5+bu9ryjTaLbNdH/Cmf5/8WrmgJ3vG2Ksk796J7qsVddwvQkZ
n6NwDm2Tm+ETMCG85yEA3jl4Kr9RXfWHYWEavv0vsq6M+bUSSq7VJAhgk4wfx6qJ
BnFX2qKpODeYLHaHxU1EnIXrStNfIqR4Eu0Xre8jAkzrDdaFy/KnX4HGgNdz413C
XzBCKEuu3VJj07ZvonnTzOgoLvh8+PCoQ2M4OBPT9gHqUov1I8nWnrjc+HuM1BW3
YIGCB5TV9x0Y7hjvkr4E38gbJURjuDwg8jof4lMRmU/FHXFLt1ucGwNFUJdPwI7d
yEKRA03Lr7htfP5sa9tmv3L93dKDpo1gW1LsfiM3Cur5jARM/hBA+eYJr12Laf9o
L59r8JmweqF3zRSwGSY336XoR/Fv/PAFs9vfKKWZp0uiRtuY9JZNRTF8trnfNf19
57bND+DS2HWPmWkw4yK6CGa0s55XadiDt4gDFvKjl68dBWZoHutY+cZy/hK1D5uq
agcX1kzbr/Pzy1gsq9FBBwaTJqBuYIAsSuzP+7NNZXoPd3rg13V93pbZr8eQN5VO
QIBZK83xZEtHSJBEdUSuBOo3JS7j/rjEnspRqOI4soFnx1vaK0TrRyzJ5KBOuGpW
4u8/ZUdIq8KIE30Mj/XI/sgAPr5jUQIDAQABAoICAQCqIgbFcqwcK7zWBgWrFsD3
53u4J4t4+df6NGB7F9CAtdgKlej1XDl8gI46Em89HLwqyOdPhCD3opoR3Vg69+IX
f62+gSD+SrA4A7jFxXvryXt0g3hTHYFHssx2j39NUghxOrOvxm6bgxJ4ifqt+Uq8
cEtM26Xu/T4/3xTpN+7pnVBHGzmLe1q8RNiLe5qhmwtgz/ZKmdSnz0YLQDRo5jWf
Xhxkb63WKrFIu4JzV9my/v9/GfMdHxD0a196ZqHLX0Buj4pQuVbS18dxLF94qIXC
FCZtYtpAxmhjOR2btJ/M1S2MBMkR3vRvSOuxHd8d/zdYys5k2WElArs1TDGGDldW
jp3FYkoygsdWTs056HM1Y9F8dV2KAWfAhEQD8mBIGVjMrCqpnyZcK6JkqVg9c7YW
IYQ2JRwsHq58FMNa3TLTvf/OClhEfSbRWAF0AhMTpnSUgP06cbJeXyzqzHdE37hv
74OBx7KNoS+PEQ3lVgbHsWoUzf3SqB1IOzLyzuEUgHqON2GKmmCNcRMBi3DuV9tw
Q8LWynNxhD8vyBkmo0kAd/FwgXrxJTGdYvxyn29I7QanCTH7o8wtjSE0jj9Qo7oC
McAYGR6oTAjrT78KhI7aZJU5nuA6ySSCJRa6et1CC+SseWknyMMJ5HTo8l7jjXJA
9hjNGGs6giOxznizf+2YAQKCAQEA9wRQk4yN402tfuicvfQBnFUtcpqctWSgGc0T
qzWJgH/W07FMUHzAvqCgsYMMaeteXOMZH7jijvtIlhYfIg5w+RJ9PSsSu680OzGN
R31+l2B/QzRAHUJ6+OVgWxAn6awU1mYLaiwVmSNWEnjAPE4XeSK708OOganI3pBQ
8zOHj+j6uV8ddG79D6FqNJHAQwpou/p+XO/BGDFgX22x4F68Z0gCQcmoyAE7ppOp
dqq3lPoDbRQ02/5cqaIA6dhmfjK2cpz4y1nUxffzY7qJjpoB/YSdR66cCNiYcJzp
fMVBXhF9Iyj/Cah1w+hc0NOy9dW15afFaLFK0zrtAzEaVxH/0QKCAQEAwRPOwSCl
XrMYXmc91TF6XbhErILHK/pIEOIMF09KNJvSjY0188Ram/pFbPRYh0cIyASmRGXL
Qq5B1Qi0vx5TCq1OCrW2yeE7zboAlnADhk1u9N8YmL6JrCKVGQO7wFD3V8uphXdM
tixNa5WvJ6eE5Vq+SVy99V5pQgb8ErrISlW4MYK7LI7DruSDuM2tHtiOcXcdTVej
1stXJZkH46RYvxxid9tRzfiB8K5ziZfLwPNf2wRyj1J4ojn5pPNhhfkjJ24LCZGt
JxwSXqdP+4x7by6x3mU+hutU/lF3jl+0edSnU0cZ6lvuq2T5YGgda/VXlv1ZFQUw
rwUXD9unU+aLgQKCAQEA9R74/pI5sthAVHFsKStb9dComtNGstI59aCF5h3oZvV1
Lvj/q9dARWqMS9qplOoV58MMCWikmhJNw3IMTvVZsjBgyzRVEJ4aDKttcQXde0Ys
w3m0LdTsxtSHu5XapY032FHG/gLlI+Pm48mjqbQsou6OyOOEJLNhO0qmqc/2tB4T
v6PdTM9enAYnqCcCTQSlTfSTNJJOYT2OTuRB4U7hUvQoGTSOInrmwLRDNBjQuCso
/zNQCQbu2P6EPYmam5yjZDTUxqZL+G/GvK49Fp9JXlQc5ycke7rD+uwa3s+3wCtG
rH9gJitfQZrxj+Cj9EOwj0bfJLbac6ZD0CkH5GNeIQKCAQBdoGFOPapzdZ2HicDu
NQQFlmmWzgQPS1rO9Q6v7v8o67b6dVOIVdsqb/5ii0qyrruPYtHNsR8TwrShvYsI
cogKUWfawatV0ibR6DSIvuC2q632iIjA6QSRuGNcsfbFl32Z0WTvF57XaDxSw08g
h5dmMM69fH+REKsyHXj3DCQ8B70+JQrm3IP/t0g4wWQF5TWNyBkpfCoy6n/j94Vf
2j4+zmDhhjTxEGTSdYYJXtarRllhN5Ll9TQSVtK8LllIQjvNzwsDJOU2ZeJyi+e5
L7Jbg+U01xuvCUc52/+Bxt8ZhQlu1Le4ccQW0Ows19AMnfhPe6NLEi09cdZxFi7Z
/J4BAoIBABCzkBDFxZdfWYt69VBt9PSG8eJ6avny3hXCtKaHIQb+aD5nKjRP0DVh
gyutCo6RasMEc6D1tJGyR/Xvhm64q4JPb5UbSaRQiVYKdgRtMM9pZeBkcBtNs18K
yMx5ajgYorrbi86hXHX7q+JYP8MCbcqqAUSl/Hi8nPxc1foTiCNDf4kGoHvXmoxt
0tA65tFFQhEA6KBn68SDkyTsl/zb5Sx0GJY4kZkOeF3GaxPFX12skgXv95GJUskX
88RJsH4Qqqtzbzj8R241BH8OrcOoyELc6xPioEqUHKVxSIf2ylITbj0UQHd2u0mN
tajKl+aoc+CDxUYbilzhhKetWWF/cJY=
-----END PRIVATE KEY-----

View File

@ -1,32 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIFhjCCA26gAwIBAgIJALJdsE+BUxypMA0GCSqGSIb3DQEBCwUAMF8xCzAJBgNV
BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0G
A1UECgwGRG9ja2VyMRowGAYDVQQDDBFOb3RhcnkgVGVzdGluZyBDQTAeFw0xNzAx
MjMwNjAzMzZaFw0yNzAxMjEwNjAzMzZaMF8xCzAJBgNVBAYTAlVTMQswCQYDVQQI
DAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGRG9ja2VyMRow
GAYDVQQDDBFOb3RhcnkgVGVzdGluZyBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBALIZNBcIoQDJql5w+XULXq9W3tmD47xnf+IG4u7hkDVPCT4xRG74
LBoSuFyPUrfT+tsibMlNG6XRtSfLQdNNeQuyIuiilNXV0kXB0RR3TrhxCaKdhRU5
oQGfpYMvbPNFB7WU/5aAiQutHH85hEMPECf1qPjq8YlUaXJLGFY3WRkW+OOBZ78U
00PqKlvC1kR/NbsV3IkMrO+vWWJQrPFusyYjQ511eQXnRtt8P0Qic0azPffQDVxC
WUe47hmdQ1AULbxQ9AZcPlMI7UFqo+/w/4hPEGJMeOWirLvHLXg4nsOwy7DfWl/n
MqLdJOC/KNfQVAQtkteeZZkkIIV1gxTPYsJqPNwkP9GdJK1A8NW1ef75v7xbQCPY
03QQonBEK7ny7b1xXGGgJzXvK9RP0UUwjt/815c4d0cgUHsy4yuvl2F44EObRshk
fjJVsN/0wrtq4QLE5ZvbeO+7to8dLcRxkmB8axhxahega7akUyY0WxZ+iSn6fzft
/xeCcs/L10V5z0kK4PbiNnooDzV4B6Dy/5oyNExw0jgpD0mzOK5aLb0tXGqFT/ZJ
9vydelBq5q4jLV7SHhHM1dBJSv1fl7vOpDlEr7LBd4YAO2BowoyGLHtLhgYybXF+
CZ9ywPb1dIIcdK5IVeZECNHMSBuhCRZUu+aun8tRcdSgLEX7mQ/GKWELAgMBAAGj
RTBDMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgFGMB0GA1UdDgQW
BBSWWbcCebeEgZlWk2/k+abh+bEFpDANBgkqhkiG9w0BAQsFAAOCAgEAQ9gA3Q4b
r2+ZJdIDoDzCNdtHQbb/d1NiUP/Na1MFo7omR3MnKGXy3dIp9IrQq6ROhlqUhDvl
pZegYhTbunTVv1KKJ+5n1hY6pG/Jr8oLY3b9i4qwDLKfQGm5PmrfwAtqbLSfY2M0
2AZyAhCdGbqB7WpTdG1J7DzGbVVWAtS05e24Mu0qZJvpHdtl4+t89vXgJ/bPrPxF
cpAlT9DOtobTEqrXZeS937F1qNyIgyBki+7mtxkwng5cf3zQM2BJ9lSFQJOBSRDr
haMcnaPI4pknO7OfYf5W9LaS1Dx/U/NeMBfnVBd9NjUw+TMjy2MdMLUaLa9EF7Jo
Gjk+fKaTaUgO8I487wHPMeoEA4A4dEePzGrybRLfl1ZYGQ0xcgunz64n2xfQIy2y
swiyaofYlLxzHzOL0N+Y76P0ic37t9R2F5ggNhfbXhClK2h4HmdjRRRt3VkxR4AD
7OM09bEhlZby34HOlCaC0PHKwYBMjneAG3ycPN88YTMYR2/KizExe71ayNwX2KHL
ib1nOZgZT6s+YvgsZ7lRmMD4iqjuAEh5SRAcWlolVif8bAy09BkY1vwrtgV73q88
heEbsCE1fsfk1OfH5W4yjjiSDZFRt5oTCPQWJp+2P0RJ9LCxcbf0RrCg3hg5rD9N
lVTA0dsixv5zF3wTuad9inhk9Rmlq1KoaqA=
-----END CERTIFICATE-----

View File

@ -6,7 +6,7 @@
"type": "remote",
"hostname": "notarysigner",
"port": "7899",
"tls_ca_file": "./root-ca.crt",
"tls_ca_file": "./notary-signer-ca.crt",
"key_algorithm": "ecdsa"
},
"logging": {

View File

@ -60,6 +60,8 @@ services:
- TERM=dumb
- MYSQL_ALLOW_EMPTY_PASSWORD="true"
command: mysqld --innodb_file_per_table
depends_on:
- log
logging:
driver: "syslog"
options:

View File

@ -1,7 +1,7 @@
version: '2'
services:
log:
image: vmware/harbor-log
image: vmware/harbor-log:__version__
container_name: harbor-log
restart: always
volumes:
@ -11,7 +11,7 @@ services:
networks:
- harbor
registry:
image: registry:2.6.0
image: vmware/registry:photon-2.6.0
container_name: registry
restart: always
volumes:
@ -31,7 +31,7 @@ services:
syslog-address: "tcp://127.0.0.1:1514"
tag: "registry"
mysql:
image: vmware/harbor-db
image: vmware/harbor-db:__version__
container_name: harbor-db
restart: always
volumes:
@ -48,7 +48,7 @@ services:
syslog-address: "tcp://127.0.0.1:1514"
tag: "mysql"
adminserver:
image: vmware/harbor-adminserver
image: vmware/harbor-adminserver:__version__
container_name: harbor-adminserver
env_file:
- ./common/config/adminserver/env
@ -67,7 +67,7 @@ services:
syslog-address: "tcp://127.0.0.1:1514"
tag: "adminserver"
ui:
image: vmware/harbor-ui
image: vmware/harbor-ui:__version__
container_name: harbor-ui
env_file:
- ./common/config/ui/env
@ -88,7 +88,7 @@ services:
syslog-address: "tcp://127.0.0.1:1514"
tag: "ui"
jobservice:
image: vmware/harbor-jobservice
image: vmware/harbor-jobservice:__version__
container_name: harbor-jobservice
env_file:
- ./common/config/jobservice/env

View File

@ -20,19 +20,10 @@ max_job_workers = 3
#Determine whether or not to generate certificate for the registry's token.
#If the value is on, the prepare script creates new root cert and private key
#for generating token to access the registry. If the value is off, a key/certificate must
#be supplied for token generation.
#for generating token to access the registry. If the value is off the default key/cert will be used.
#This flag also controls the creation of the notary signer's cert.
customize_crt = on
#Information of your organization for certificate
crt_country = CN
crt_state = State
crt_location = CN
crt_organization = organization
crt_organizationalunit = organizational unit
crt_commonname = example.com
crt_email = example@example.com
#The path of cert and key files for nginx, they are applied only the protocol is set to https
ssl_cert = /data/cert/server.crt
ssl_cert_key = /data/cert/server.key

View File

@ -166,13 +166,13 @@ then
if [ -n "$(docker-compose -f docker-compose.yml -f docker-compose.notary.yml ps -q)" ]
then
note "stopping existing Harbor instance ..."
docker-compose -f docker-compose.yml -f docker-compose.notary.yml down
docker-compose -f docker-compose.yml -f docker-compose.notary.yml down -v
fi
else
if [ -n "$(docker-compose -f docker-compose.yml ps -q)" ]
then
note "stopping existing Harbor instance ..."
docker-compose -f docker-compose.yml down
docker-compose -f docker-compose.yml down -v
fi
fi
echo ""

View File

@ -135,13 +135,6 @@ if protocol == "https":
cert_path = rcp.get("configuration", "ssl_cert")
cert_key_path = rcp.get("configuration", "ssl_cert_key")
customize_crt = rcp.get("configuration", "customize_crt")
crt_country = rcp.get("configuration", "crt_country")
crt_state = rcp.get("configuration", "crt_state")
crt_location = rcp.get("configuration", "crt_location")
crt_organization = rcp.get("configuration", "crt_organization")
crt_organizationalunit = rcp.get("configuration", "crt_organizationalunit")
crt_commonname = rcp.get("configuration", "crt_commonname")
crt_email = rcp.get("configuration", "crt_email")
max_job_workers = rcp.get("configuration", "max_job_workers")
token_expiration = rcp.get("configuration", "token_expiration")
verify_remote_cert = rcp.get("configuration", "verify_remote_cert")
@ -262,52 +255,54 @@ FNULL = open(os.devnull, 'w')
from functools import wraps
def stat_decorator(func):
@wraps(func)
def check_wrapper(*args, **kwargs):
stat = func(*args, **kwargs)
message = "Generated configuration file: %s" % kwargs['path'] \
if stat == 0 else "Fail to generate %s" % kwargs['path']
def check_wrapper(*args, **kw):
stat = func(*args, **kw)
message = "Generated certificate, key file: %s, cert file: %s" % (kw['key_path'], kw['cert_path']) \
if stat == 0 else "Fail to generate key file: %s, cert file: %s" % (kw['key_path'], kw['cert_path'])
print(message)
if stat != 0:
sys.exit(1)
return check_wrapper
@stat_decorator
def check_private_key_stat(*args, **kwargs):
return subprocess.call(["openssl", "genrsa", "-out", kwargs['path'], "4096"],\
stdout=FNULL, stderr=subprocess.STDOUT)
def create_root_cert(subj, key_path="./k.key", cert_path="./cert.crt"):
rc = subprocess.call(["openssl", "genrsa", "-out", key_path, "4096"], stdout=FNULL, stderr=subprocess.STDOUT)
if rc != 0:
return rc
return subprocess.call(["openssl", "req", "-new", "-x509", "-key", key_path,\
"-out", cert_path, "-days", "3650", "-subj", subj], stdout=FNULL, stderr=subprocess.STDOUT)
@stat_decorator
def check_certificate_stat(*args, **kwargs):
dirty_subj = "/C={0}/ST={1}/L={2}/O={3}/OU={4}/CN={5}/emailAddress={6}"\
.format(crt_country, crt_state, crt_location, crt_organization,\
crt_organizationalunit, crt_commonname, crt_email)
subj = validate_crt_subj(dirty_subj)
return subprocess.call(["openssl", "req", "-new", "-x509", "-key",\
private_key_pem, "-out", root_crt, "-days", "3650", "-subj", subj], \
stdout=FNULL, stderr=subprocess.STDOUT)
def create_cert(subj, ca_key, ca_cert, key_path="./k.key", cert_path="./cert.crt"):
cert_dir = os.path.dirname(cert_path)
csr_path = os.path.join(cert_dir, "tmp.csr")
rc = subprocess.call(["openssl", "req", "-newkey", "rsa:4096", "-nodes","-sha256","-keyout", key_path,\
"-out", csr_path, "-subj", subj], stdout=FNULL, stderr=subprocess.STDOUT)
if rc != 0:
return rc
return subprocess.call(["openssl", "x509", "-req", "-days", "3650", "-in", csr_path, "-CA", \
ca_cert, "-CAkey", ca_key, "-CAcreateserial", "-out", cert_path], stdout=FNULL, stderr=subprocess.STDOUT)
def openssl_is_installed(stat):
if stat == 0:
return True
else:
def openssl_installed():
shell_stat = subprocess.check_call(["which", "openssl"], stdout=FNULL, stderr=subprocess.STDOUT)
if shell_stat != 0:
print("Cannot find openssl installed in this computer\nUse default SSL certificate file")
return False
return True
if customize_crt == 'on':
if customize_crt == 'on' and openssl_installed():
shell_stat = subprocess.check_call(["which", "openssl"], stdout=FNULL, stderr=subprocess.STDOUT)
if openssl_is_installed(shell_stat):
private_key_pem = os.path.join(config_dir, "ui", "private_key.pem")
root_crt = os.path.join(config_dir, "registry", "root.crt")
check_private_key_stat(path=private_key_pem)
check_certificate_stat(path=root_crt)
empty_subj = "/C=/ST=/L=/O=/CN=/"
private_key_pem = os.path.join(config_dir, "ui", "private_key.pem")
root_crt = os.path.join(config_dir, "registry", "root.crt")
create_root_cert(empty_subj, key_path=private_key_pem, cert_path=root_crt)
else:
print("Generated configuration file: %s" % ui_config_dir + "private_key.pem")
print("Copied configuration file: %s" % ui_config_dir + "private_key.pem")
shutil.copyfile(os.path.join(templates_dir, "ui", "private_key.pem"), os.path.join(ui_config_dir, "private_key.pem"))
print("Generated configuration file: %s" % registry_config_dir + "root.crt")
print("Copied configuration file: %s" % registry_config_dir + "root.crt")
shutil.copyfile(os.path.join(templates_dir, "registry", "root.crt"), os.path.join(registry_config_dir, "root.crt"))
FNULL.close()
if args.notary_mode:
notary_config_dir = prep_conf_dir(config_dir, "notary")
notary_temp_dir = os.path.join(templates_dir, "notary")
@ -315,11 +310,27 @@ if args.notary_mode:
if os.path.exists(os.path.join(notary_config_dir, "mysql-initdb.d")):
shutil.rmtree(os.path.join(notary_config_dir, "mysql-initdb.d"))
shutil.copytree(os.path.join(notary_temp_dir, "mysql-initdb.d"), os.path.join(notary_config_dir, "mysql-initdb.d"))
#TODO:generate certs?
print("Copying certs for notary signer")
shutil.copy2(os.path.join(notary_temp_dir, "notary-signer.crt"), notary_config_dir)
shutil.copy2(os.path.join(notary_temp_dir, "notary-signer.key"), notary_config_dir)
shutil.copy2(os.path.join(notary_temp_dir, "root-ca.crt"), notary_config_dir)
if customize_crt == 'on' and openssl_installed():
temp_cert_dir = os.path.join(base_dir, "cert_tmp")
if not os.path.exists(temp_cert_dir):
os.makedirs(temp_cert_dir)
ca_subj = "/C=US/ST=California/L=Palo Alto/O=VMware, Inc./OU=Harbor/CN=Self-signed by VMware, Inc."
cert_subj = "/C=US/ST=California/L=Palo Alto/O=VMware, Inc./OU=Harbor/CN=notarysigner"
signer_ca_cert = os.path.join(temp_cert_dir, "notary-signer-ca.crt")
signer_ca_key = os.path.join(temp_cert_dir, "notary-signer-ca.key")
signer_cert_path = os.path.join(temp_cert_dir, "notary-signer.crt")
signer_key_path = os.path.join(temp_cert_dir, "notary-signer.key")
create_root_cert(ca_subj, key_path=signer_ca_key, cert_path=signer_ca_cert)
create_cert(cert_subj, signer_ca_key, signer_ca_cert, key_path=signer_key_path, cert_path=signer_cert_path)
print("Copying certs for notary signer")
shutil.copy2(signer_cert_path, notary_config_dir)
shutil.copy2(signer_key_path, notary_config_dir)
shutil.copy2(signer_ca_cert, notary_config_dir)
else:
print("Copying certs for notary signer")
shutil.copy2(os.path.join(notary_temp_dir, "notary-signer.crt"), notary_config_dir)
shutil.copy2(os.path.join(notary_temp_dir, "notary-signer.key"), notary_config_dir)
shutil.copy2(os.path.join(notary_temp_dir, "notary-signer-ca.crt"), notary_config_dir)
shutil.copy2(os.path.join(registry_config_dir, "root.crt"), notary_config_dir)
print("Copying notary signer configuration file")
@ -335,6 +346,6 @@ if args.notary_mode:
default_alias = ''.join(random.choice(string.ascii_letters) for i in range(8))
render(os.path.join(notary_temp_dir, "signer_env"), os.path.join(notary_config_dir, "signer_env"), alias = default_alias)
FNULL.close()
print("The configuration files are ready, please use docker-compose to start the service.")

View File

@ -0,0 +1,21 @@
<!--
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.
-->
<!DOCTYPE html>
<html>
<body>
<p>Please click this link to reset your password:</p>
<a href="{{.URL}}/reset_password?reset_uuid={{.UUID}}">{{.URL}}/reset_password?reset_uuid={{.UUID}}</a>
</body>
</html>

View File

@ -120,15 +120,8 @@ export class HarborShellComponent implements OnInit, OnDestroy {
//Handle the global search event and then let the result page to trigger api
doSearch(event: string): void {
if (event === "") {
if (!this.isSearchResultsOpened) {
//Will not open search result panel if term is empty
return;
} else {
//If opened, then close the search result panel
this.isSearchResultsOpened = false;
this.searchResultComponet.close();
return;
}
//Do nothing
return;
}
//Once this method is called
//the search results page must be opened

View File

@ -0,0 +1,62 @@
<div class="config-container">
<h2 style="display: inline-block;" class="custom-h2">{{'CONFIG.TITLE' | translate }}</h2>
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
<clr-tabs (clrTabsCurrentTabLinkChanged)="tabLinkChanged($event)">
<clr-tab-link [clrTabLinkId]="'config-auth'" [clrTabLinkActive]='isCurrentTabLink("config-auth")'>{{'CONFIG.AUTH' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-replication'" [clrTabLinkActive]='isCurrentTabLink("config-replication")'>{{'CONFIG.REPLICATION' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-email'" [clrTabLinkActive]='isCurrentTabLink("config-email")'>{{'CONFIG.EMAIL' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-system'" [clrTabLinkActive]='isCurrentTabLink("config-system")'>{{'CONFIG.SYSTEM' | translate }}</clr-tab-link>
<clr-tab-content [clrTabContentId]="'authentication'" [clrTabContentActive]='isCurrentTabContent("authentication")'>
<config-auth [ldapConfig]="allConfig"></config-auth>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'replication'" [clrTabContentActive]='isCurrentTabContent("replication")'>
<form #repoConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="verifyRemoteCert">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</label>
<clr-checkbox name="verifyRemoteCert" id="verifyRemoteCert" [(ngModel)]="allConfig.verify_remote_cert.value" [disabled]="disabled(allConfig.verify_remote_cert)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-right" style="top:-8px;">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate }}</span>
</a>
</clr-checkbox>
</div>
</section>
</form>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'email'" [clrTabContentActive]='isCurrentTabContent("email")'>
<config-email [mailConfig]="allConfig"></config-email>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'system_settings'" [clrTabContentActive]='isCurrentTabContent("system_settings")'>
<form #systemConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="tokenExpiration" class="required">{{'CONFIG.TOKEN_EXPIRATION' | translate}}</label>
<label for="tokenExpiration" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="tokenExpirationInput.invalid && (tokenExpirationInput.dirty || tokenExpirationInput.touched)">
<input name="tokenExpiration" type="text" #tokenExpirationInput="ngModel" [(ngModel)]="allConfig.token_expiration.value"
required
pattern="^[1-9]{1}[\d]*$"
id="tokenExpiration"
size="40" [disabled]="disabled(allConfig.token_expiration)">
<span class="tooltip-content">
{{'TOOLTIP.NUMBER_REQUIRED' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
</a>
</div>
</section>
</form>
</clr-tab-content>
</clr-tabs>
<div>
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testMailServer()" *ngIf="showTestServerBtn" [disabled]="!isMailConfigValid()">{{'BUTTON.TEST_MAIL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testLDAPServer()" *ngIf="showLdapServerBtn" [disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button>
<span class="spinner spinner-inline" [hidden]="!testingInProgress"></span>
</div>
</div>

View File

@ -1,39 +1,47 @@
<div class="config-container">
<h2 style="display: inline-block;" class="custom-h2">{{'CONFIG.TITLE' | translate }}</h2>
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
<clr-tabs (clrTabsCurrentTabLinkChanged)="tabLinkChanged($event)">
<clr-tab-link [clrTabLinkId]="'config-auth'" [clrTabLinkActive]="true">{{'CONFIG.AUTH' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-replication'">{{'CONFIG.REPLICATION' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-email'">{{'CONFIG.EMAIL' | translate }}</clr-tab-link>
<clr-tab-link [clrTabLinkId]="'config-system'">{{'CONFIG.SYSTEM' | translate }}</clr-tab-link>
<clr-tab-content [clrTabContentId]="'authentication'" [clrTabContentActive]="true">
<config-auth [ldapConfig]="allConfig"></config-auth>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'replication'">
<form #repoConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="verifyRemoteCert">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</label>
<clr-checkbox name="verifyRemoteCert" id="verifyRemoteCert" [(ngModel)]="allConfig.verify_remote_cert.value" [disabled]="disabled(allConfig.verify_remote_cert)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-right" style="top:-8px;">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate }}</span>
</a>
</clr-checkbox>
</div>
</section>
</form>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'email'">
<config-email [mailConfig]="allConfig"></config-email>
</clr-tab-content>
<clr-tab-content [clrTabContentId]="'system_settings'">
<form #systemConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="tokenExpiration" class="required">{{'CONFIG.TOKEN_EXPIRATION' | translate}}</label>
<label for="tokenExpiration" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="tokenExpirationInput.invalid && (tokenExpirationInput.dirty || tokenExpirationInput.touched)">
<ul id="configTabs" class="nav" role="tablist">
<li role="presentation" class="nav-item">
<button id="config-auth" class="btn btn-link nav-link active" aria-controls="authentication" [class.active]='isCurrentTabLink("config-auth")' type="button" (click)='tabLinkClick("config-auth")'>{{'CONFIG.AUTH' | translate }}</button>
</li>
<li role="presentation" class="nav-item">
<button id="config-replication" class="btn btn-link nav-link" aria-controls="replication" [class.active]='isCurrentTabLink("config-replication")' type="button" (click)='tabLinkClick("config-replication")'>{{'CONFIG.REPLICATION' | translate }}</button>
</li>
<li role="presentation" class="nav-item">
<button id="config-email" class="btn btn-link nav-link" aria-controls="email" [class.active]='isCurrentTabLink("config-email")' type="button" (click)='tabLinkClick("config-email")'>{{'CONFIG.EMAIL' | translate }}</button>
</li>
<li role="presentation" class="nav-item">
<button id="config-system" class="btn btn-link nav-link" aria-controls="system_settings" [class.active]='isCurrentTabLink("config-system")' type="button" (click)='tabLinkClick("config-system")'>{{'CONFIG.SYSTEM' | translate }}</button>
</li>
</ul>
<section id="authentication" role="tabpanel" aria-labelledby="config-auth" [hidden]='!isCurrentTabContent("authentication")'>
<config-auth [ldapConfig]="allConfig"></config-auth>
</section>
<section id="replication" role="tabpanel" aria-labelledby="config-replication" [hidden]='!isCurrentTabContent("replication")'>
<form #repoConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="verifyRemoteCert">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</label>
<clr-checkbox name="verifyRemoteCert" id="verifyRemoteCert" [(ngModel)]="allConfig.verify_remote_cert.value" [disabled]="disabled(allConfig.verify_remote_cert)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-right" style="top:-8px;">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate }}</span>
</a>
</clr-checkbox>
</div>
</section>
</form>
</section>
<section id="email" role="tabpanel" aria-labelledby="config-email" [hidden]='!isCurrentTabContent("email")'>
<config-email [mailConfig]="allConfig"></config-email>
</section>
<section id="system_settings" role="tabpanel" aria-labelledby="config-system" [hidden]='!isCurrentTabContent("system_settings")'>
<form #systemConfigFrom="ngForm" class="form">
<section class="form-block">
<div class="form-group">
<label for="tokenExpiration" class="required">{{'CONFIG.TOKEN_EXPIRATION' | translate}}</label>
<label for="tokenExpiration" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="tokenExpirationInput.invalid && (tokenExpirationInput.dirty || tokenExpirationInput.touched)">
<input name="tokenExpiration" type="text" #tokenExpirationInput="ngModel" [(ngModel)]="allConfig.token_expiration.value"
required
pattern="^[1-9]{1}[\d]*$"
@ -43,15 +51,14 @@
{{'TOOLTIP.NUMBER_REQUIRED' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
</a>
</div>
</section>
</form>
</clr-tab-content>
</clr-tabs>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
</a>
</div>
</section>
</form>
</section>
<div>
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>

View File

@ -16,8 +16,15 @@ import { ConfigurationAuthComponent } from './auth/config-auth.component';
import { ConfigurationEmailComponent } from './email/config-email.component';
import { AppConfigService } from '../app-config.service';
import { SessionService } from '../shared/session.service';
const fakePass = "fakepassword";
const TabLinkContentMap = {
"config-auth": "authentication",
"config-replication": "replication",
"config-email": "email",
"config-system": "system_settings"
};
@Component({
selector: 'config',
@ -27,7 +34,7 @@ const fakePass = "fakepassword";
export class ConfigurationComponent implements OnInit, OnDestroy {
private onGoing: boolean = false;
allConfig: Configuration = new Configuration();
private currentTabId: string = "";
private currentTabId: string = "config-auth";//default tab
private originalCopy: Configuration;
private confirmSub: Subscription;
private testingOnGoing: boolean = false;
@ -41,17 +48,76 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
private msgService: MessageService,
private configService: ConfigurationService,
private confirmService: ConfirmationDialogService,
private appConfigService: AppConfigService) { }
private appConfigService: AppConfigService,
private session: SessionService) { }
private isCurrentTabLink(tabId: string): boolean {
return this.currentTabId === tabId;
}
private isCurrentTabContent(contentId: string): boolean {
return TabLinkContentMap[this.currentTabId] === contentId;
}
private hasUnsavedChangesOfCurrentTab(): any {
let allChanges = this.getChanges();
if (this.isEmpty(allChanges)) {
return null;
}
let properties = [];
switch (this.currentTabId) {
case "config-auth":
for (let prop in allChanges) {
if (prop.startsWith("ldap_")) {
return allChanges;
}
}
properties = ["auth_mode", "project_creation_restriction", "self_registration"];
break;
case "config-email":
for (let prop in allChanges) {
if (prop.startsWith("email_")) {
return allChanges;
}
}
return null;
case "config-replication":
properties = ["verify_remote_cert"];
break;
case "config-system":
properties = ["token_expiration"];
break;
default:
return null;
}
for (let prop in allChanges) {
if (properties.indexOf(prop) != -1) {
return allChanges;
}
}
return null;
}
ngOnInit(): void {
//First load
this.retrieveConfig();
//Double confirm the current use has admin role
let currentUser = this.session.getCurrentUser();
if (currentUser && currentUser.has_admin_role > 0) {
this.retrieveConfig();
}
this.confirmSub = this.confirmService.confirmationConfirm$.subscribe(confirmation => {
if (confirmation &&
confirmation.state === ConfirmationState.CONFIRMED &&
confirmation.source === ConfirmationTargets.CONFIG) {
this.reset(confirmation.data);
confirmation.state === ConfirmationState.CONFIRMED) {
if (confirmation.source === ConfirmationTargets.CONFIG) {
this.reset(confirmation.data);
} else if (confirmation.source === ConfirmationTargets.CONFIG_TAB) {
this.reset(confirmation.data["changes"]);
this.currentTabId = confirmation.data["tabId"];
}
}
});
}
@ -104,8 +170,15 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
return this.authConfig && this.authConfig.isValid();
}
public tabLinkChanged(tabLink: any) {
this.currentTabId = tabLink.id;
public tabLinkClick(tabLink: string) {
//Whether has unsave changes in current tab
let changes = this.hasUnsavedChangesOfCurrentTab();
if (!changes) {
this.currentTabId = tabLink;
return;
}
this.confirmUnsavedTabChanges(changes, tabLink);
}
/**
@ -154,14 +227,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
public cancel(): void {
let changes = this.getChanges();
if (!this.isEmpty(changes)) {
let msg = new ConfirmationMessage(
"CONFIG.CONFIRM_TITLE",
"CONFIG.CONFIRM_SUMMARY",
"",
changes,
ConfirmationTargets.CONFIG
);
this.confirmService.openComfirmDialog(msg);
this.confirmUnsavedChanges(changes);
} else {
//Inprop situation, should not come here
console.error("Nothing changed");
@ -218,6 +284,33 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
});
}
private confirmUnsavedChanges(changes: any) {
let msg = new ConfirmationMessage(
"CONFIG.CONFIRM_TITLE",
"CONFIG.CONFIRM_SUMMARY",
"",
changes,
ConfirmationTargets.CONFIG
);
this.confirmService.openComfirmDialog(msg);
}
private confirmUnsavedTabChanges(changes: any, tabId: string){
let msg = new ConfirmationMessage(
"CONFIG.CONFIRM_TITLE",
"CONFIG.CONFIRM_SUMMARY",
"",
{
"changes": changes,
"tabId": tabId
},
ConfirmationTargets.CONFIG_TAB
);
this.confirmService.openComfirmDialog(msg);
}
private retrieveConfig(): void {
this.onGoing = true;
this.configService.getConfiguration()

View File

@ -18,6 +18,7 @@ export class MessageComponent implements OnInit {
globalMessage: Message = new Message();
globalMessageOpened: boolean;
messageText: string = "";
private timer: any = null;
constructor(
private messageService: MessageService,
@ -48,7 +49,7 @@ export class MessageComponent implements OnInit {
// Make the message alert bar dismiss after several intervals.
//Only for this case
setInterval(() => this.onClose(), dismissInterval);
this.timer = setTimeout(() => this.onClose(), dismissInterval);
}
);
}
@ -56,15 +57,9 @@ export class MessageComponent implements OnInit {
//Translate or refactor the message shown to user
translateMessage(msg: Message): void {
if (!msg) {
return;
}
let key = "";
if (!msg.message) {
key = "UNKNOWN_ERROR";
} else {
key = typeof msg.message === "string" ? msg.message.trim() : msg.message;
let key = "UNKNOWN_ERROR", param = "";
if (msg && msg.message) {
key = (typeof msg.message === "string" ? msg.message.trim() : msg.message);
if (key === "") {
key = "UNKNOWN_ERROR";
}
@ -73,13 +68,11 @@ export class MessageComponent implements OnInit {
//Override key for HTTP 401 and 403
if (this.globalMessage.statusCode === httpStatusCode.Unauthorized) {
key = "UNAUTHORIZED_ERROR";
}
if (this.globalMessage.statusCode === httpStatusCode.Forbidden) {
} else if (this.globalMessage.statusCode === httpStatusCode.Forbidden) {
key = "FORBIDDEN_ERROR";
}
}
this.translate.get(key).subscribe((res: string) => this.messageText = res);
this.translate.get(key, { 'param': param }).subscribe((res: string) => this.messageText = res);
}
public get needAuth(): boolean {
@ -98,6 +91,9 @@ export class MessageComponent implements OnInit {
}
onClose() {
if (this.timer) {
clearTimeout(this.timer);
}
this.globalMessageOpened = false;
}
}

View File

@ -33,6 +33,8 @@ import { AuthCheckGuard } from './shared/route/auth-user-activate.service';
import { SignInGuard } from './shared/route/sign-in-guard-activate.service';
import { LeavingConfigRouteDeactivate } from './shared/route/leaving-config-deactivate.service';
import { MemberGuard } from './shared/route/member-guard-activate.service';
const harborRoutes: Routes = [
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
{ path: 'password-reset', component: ResetPasswordComponent },
@ -74,11 +76,15 @@ const harborRoutes: Routes = [
},
{
path: 'tags/:id/:repo',
component: TagRepositoryComponent
component: TagRepositoryComponent,
resolve: {
projectResolver: ProjectRoutingResolver
}
},
{
path: 'projects/:id',
component: ProjectDetailComponent,
canActivate: [MemberGuard],
resolve: {
projectResolver: ProjectRoutingResolver
},
@ -89,7 +95,8 @@ const harborRoutes: Routes = [
},
{
path: 'replication',
component: ReplicationComponent
component: ReplicationComponent,
canActivate: [SystemAdminGuard]
},
{
path: 'member',

View File

@ -1,8 +1,6 @@
import { Injectable } from '@angular/core';
import { Http, Headers, RequestOptions } from '@angular/http';
import { BaseService } from '../service/base.service';
import { AuditLog } from './audit-log';
import { Observable } from 'rxjs/Observable';
@ -13,7 +11,7 @@ import 'rxjs/add/observable/throw';
export const logEndpoint = "/api/logs";
@Injectable()
export class AuditLogService extends BaseService {
export class AuditLogService {
private httpOptions = new RequestOptions({
headers: new Headers({
"Content-Type": 'application/json',
@ -21,9 +19,7 @@ export class AuditLogService extends BaseService {
})
});
constructor(private http: Http) {
super();
}
constructor(private http: Http) {}
listAuditLogs(queryParam: AuditLog): Observable<any> {
return this.http
@ -36,13 +32,12 @@ export class AuditLogService extends BaseService {
username: queryParam.username
})
.map(response => response)
.catch(error => this.handleError(error));
.catch(error => Observable.throw(error));
}
getRecentLogs(lines: number): Observable<AuditLog[]> {
return this.http.get(logEndpoint + "?lines=" + lines, this.httpOptions)
.map(response => response.json() as AuditLog[])
.catch(error => this.handleError(error));
.catch(error => Observable.throw(error));
}
}

View File

@ -2,25 +2,18 @@
margin-top: 0px !important;
}
.filter-log {
float: right;
margin-right: 24px;
position: relative;
top: 8px;
}
.action-head-pos {
position: relative;
top: 20px;
padding-right: 18px;
}
.refresh-btn {
position: absolute;
right: -4px;
top: 8px;
cursor: pointer;
}
.refresh-btn:hover {
color: #00bfff;
}
.custom-lines-button {
padding: 0px !important;
min-width: 25px !important;
@ -29,4 +22,21 @@
.lines-button-toggole {
font-size: 16px;
text-decoration: underline;
}
.log-select {
width: 180px;
display: inline-block;
top: 1px;
}
.item-divider {
height: 24px;
display: inline-block;
width: 1px;
background-color: #ccc;
opacity: 0.55;
margin-left: 12px;
top: 8px;
position: relative;
}

View File

@ -1,20 +1,24 @@
<div>
<h2 class="h2-log-override">{{'SIDE_NAV.LOGS' | translate}}</h2>
<div class="action-head-pos">
<span>
<label>{{'RECENT_LOG.SUB_TITLE' | translate}} </label>
<button type="submit" class="btn btn-link custom-lines-button" [class.lines-button-toggole]="lines === 10" (click)="setLines(10)">10</button>
<label> | </label>
<button type="submit" class="btn btn-link custom-lines-button" [class.lines-button-toggole]="lines === 25" (click)="setLines(25)">25</button>
<label> | </label>
<button type="submit" class="btn btn-link custom-lines-button" [class.lines-button-toggole]="lines === 50" (click)="setLines(50)">50</button>
<label>{{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</label>
</span>
<grid-filter class="filter-log" filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilter($event)"></grid-filter>
<span class="refresh-btn" (click)="refresh()">
<h2 class="h2-log-override">{{'SIDE_NAV.LOGS' | translate}}
<span class="badge badge-info">{{logNumber}}</span>
</h2>
<div class="row flex-items-xs-between flex-items-xs-bottom">
<div></div>
<div class="action-head-pos">
<div class="select log-select">
<select id="log_display_num" (change)="handleOnchange($event)">
<option value="10">{{'RECENT_LOG.SUB_TITLE' | translate}} 10 {{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</option>
<option value="25">{{'RECENT_LOG.SUB_TITLE' | translate}} 25 {{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</option>
<option value="50">{{'RECENT_LOG.SUB_TITLE' | translate}} 50 {{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</option>
</select>
</div>
<div class="item-divider"></div>
<grid-filter filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilter($event)"></grid-filter>
<span (click)="refresh()" class="refresh-btn">
<clr-icon shape="refresh" [hidden]="inProgress" ng-disabled="inProgress"></clr-icon>
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
</span>
</span>
</div>
</div>
<div>
<clr-datagrid>

View File

@ -34,17 +34,22 @@ export class RecentLogComponent implements OnInit {
this.retrieveLogs();
}
public get inProgress(): boolean {
return this.onGoing;
private handleOnchange($event: any) {
if (event && event.target && event.srcElement["value"]) {
this.lines = event.srcElement["value"];
if (this.lines < 10) {
this.lines = 10;
}
this.retrieveLogs();
}
}
public setLines(lines: number): void {
this.lines = lines;
if (this.lines < 10) {
this.lines = 10;
}
public get logNumber(): number {
return this.recentLogs?this.recentLogs.length:0;
}
this.retrieveLogs();
public get inProgress(): boolean {
return this.onGoing;
}
public doFilter(terms: string): void {
@ -60,7 +65,7 @@ export class RecentLogComponent implements OnInit {
this.retrieveLogs();
}
public formatDateTime(dateTime: string){
public formatDateTime(dateTime: string) {
let dt: Date = new Date(dateTime);
return dt.toLocaleString();
}

View File

@ -31,6 +31,6 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!projectForm.form.valid" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="projectForm.form.invalid" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>

View File

@ -13,7 +13,6 @@ import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.com
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'create-project',
templateUrl: 'create-project.component.html',
@ -50,6 +49,7 @@ export class CreateProjectComponent implements AfterViewChecked {
.subscribe(
status=>{
this.create.emit(true);
this.messageService.announceMessage(status, 'PROJECT.CREATED_SUCCESS', AlertType.SUCCESS);
this.createProjectOpened = false;
},
error=>{

View File

@ -1,20 +1,20 @@
<clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-dg-column>{{'PROJECT.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="showRoleInfo">{{'PROJECT.ROLE' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let p of projects" [clrDgItem]="p">
<clr-dg-action-overflow *ngIf="listFullMode">
<button class="action-item" (click)="newReplicationRule(p)">{{'PROJECT.REPLICATION_RULE' | translate}}</button>
<clr-dg-action-overflow [hidden]="!listFullMode || p.current_user_role_id !== 1">
<button class="action-item" (click)="newReplicationRule(p)" [hidden]="!isSystemAdmin">{{'PROJECT.REPLICATION_RULE' | translate}}</button>
<button class="action-item" (click)="toggleProject(p)">{{'PROJECT.MAKE' | translate}} {{(p.public === 0 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}} </button>
<button class="action-item" (click)="deleteProject(p)">{{'PROJECT.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell><a href="javascript:void(0)" (click)="goToLink(p.project_id)">{{p.name}}</a></clr-dg-cell>
<clr-dg-cell>{{ (p.public === 1 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}}</clr-dg-cell>
<clr-dg-cell *ngIf="showRoleInfo">{{roleInfo[p.current_user_role_id] | translate}}</clr-dg-cell>
<clr-dg-cell>{{p.repo_count}}</clr-dg-cell>
<clr-dg-cell>{{p.creation_time}}</clr-dg-cell>
<clr-dg-cell>{{p.description}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{totalRecordCount || (projects ? projects.length : 0)}} {{'PROJECT.ITEMS' | translate}}

View File

@ -5,7 +5,7 @@ import { ProjectService } from '../project.service';
import { SessionService } from '../../shared/session.service';
import { SearchTriggerService } from '../../base/global-search/search-trigger.service';
import { ListMode } from '../../shared/shared.const';
import { ListMode, ProjectTypes, RoleInfo } from '../../shared/shared.const';
import { State } from 'clarity-angular';
@ -24,6 +24,8 @@ export class ListProjectComponent implements OnInit {
@Input() totalRecordCount: number;
pageOffset: number = 1;
@Input() filteredType: string;
@Output() paginate = new EventEmitter<State>();
@Output() toggle = new EventEmitter<Project>();
@ -31,6 +33,8 @@ export class ListProjectComponent implements OnInit {
@Input() mode: string = ListMode.FULL;
roleInfo = RoleInfo;
constructor(
private session: SessionService,
private router: Router,
@ -43,6 +47,15 @@ export class ListProjectComponent implements OnInit {
return this.mode === ListMode.FULL && this.session.getCurrentUser() != null;
}
get showRoleInfo(): boolean {
return this.listFullMode && this.filteredType === ProjectTypes[0];
}
public get isSystemAdmin(): boolean {
let account = this.session.getCurrentUser();
return account != null && account.has_admin_role > 0;
}
goToLink(proId: number): void {
this.searchTrigger.closeSearch(false);

View File

@ -19,15 +19,15 @@
<div class="form-group">
<label class="col-md-4 form-group-label-override">{{'MEMBER.ROLE' | translate}}</label>
<div class="radio">
<input type="radio" name="roleRadios" id="checkrads_project_admin" value="1" [(ngModel)]="member.role_id">
<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>
</div>
<div class="radio">
<input type="radio" name="roleRadios" id="checkrads_developer" value="2" [(ngModel)]="member.role_id">
<input type="radio" name="roleRadios" id="checkrads_developer" [value]="2" [(ngModel)]="member.role_id">
<label for="checkrads_developer">{{'MEMBER.DEVELOPER' | translate}}</label>
</div>
<div class="radio">
<input type="radio" name="roleRadios" id="checkrads_guest" value="3" [(ngModel)]="member.role_id">
<input type="radio" name="roleRadios" id="checkrads_guest" [value]="3" [(ngModel)]="member.role_id">
<label for="checkrads_guest">{{'MEMBER.GUEST' | translate}}</label>
</div>
</div>
@ -36,6 +36,6 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="memberForm.form.invalid" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>

View File

@ -51,6 +51,7 @@ export class AddMemberComponent implements AfterViewChecked {
.addMember(this.projectId, this.member.username, +this.member.role_id)
.subscribe(
response=>{
this.messageService.announceMessage(response, 'MEMBER.ADDED_SUCCESS', AlertType.SUCCESS);
console.log('Added member successfully.');
this.added.emit(true);
this.addMemberOpened = false;
@ -112,9 +113,11 @@ export class AddMemberComponent implements AfterViewChecked {
}
openAddMemberModal(): void {
this.memberForm.reset();
this.member = new Member();
this.addMemberOpened = true;
this.hasChanged = false;
this.member.role_id = 1;
}
}

View File

@ -2,7 +2,7 @@
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<div class="flex-xs-middle option-left">
<button class="btn btn-primary" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon> {{'MEMBER.MEMBER' | translate }}</button>
<button *ngIf="hasProjectAdminRole" class="btn btn-primary" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon> {{'MEMBER.MEMBER' | translate }}</button>
<add-member [projectId]="projectId" (added)="addedMember($event)"></add-member>
</div>
<div class="flex-xs-middle option-right">
@ -17,15 +17,15 @@
<clr-datagrid>
<clr-dg-column>{{'MEMBER.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'MEMBER.ROLE' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let u of members">
<clr-dg-action-overflow [hidden]="u.user_id === currentUser.user_id">
<button class="action-item" (click)="changeRole(u.user_id, 1)">{{'MEMBER.PROJECT_ADMIN' | translate}}</button>
<button class="action-item" (click)="changeRole(u.user_id, 2)">{{'MEMBER.DEVELOPER' | translate}}</button>
<button class="action-item" (click)="changeRole(u.user_id, 3)">{{'MEMBER.GUEST' | translate}}</button>
<button class="action-item" (click)="deleteMember(u.user_id)">{{'MEMBER.DELETE' | translate}}</button>
<clr-dg-row *ngFor="let m of members">
<clr-dg-action-overflow [hidden]="m.user_id === currentUser.user_id || !hasProjectAdminRole">
<button class="action-item" (click)="changeRole(m, 1)">{{'MEMBER.PROJECT_ADMIN' | translate}}</button>
<button class="action-item" (click)="changeRole(m, 2)">{{'MEMBER.DEVELOPER' | translate}}</button>
<button class="action-item" (click)="changeRole(m, 3)">{{'MEMBER.GUEST' | translate}}</button>
<button class="action-item" (click)="deleteMember(m)">{{'MEMBER.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{u.username}}</clr-dg-cell>
<clr-dg-cell>{{roleInfo[u.role_id] | translate}}</clr-dg-cell>
<clr-dg-cell>{{m.username}}</clr-dg-cell>
<clr-dg-cell>{{roleInfo[m.role_id] | translate}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (members ? members.length : 0) }} {{'MEMBER.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>

View File

@ -15,6 +15,8 @@ import { ConfirmationDialogService } from '../../shared/confirmation-dialog/conf
import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message';
import { SessionService } from '../../shared/session.service';
import { RoleInfo } from '../../shared/shared.const';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/catch';
@ -22,7 +24,7 @@ import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw';
import { Subscription } from 'rxjs/Subscription';
export const roleInfo: {} = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER', 3: 'MEMBER.GUEST' };
import { Project } from '../../project/project';
@Component({
moduleId: module.id,
@ -31,21 +33,22 @@ export const roleInfo: {} = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER',
})
export class MemberComponent implements OnInit, OnDestroy {
currentUser: SessionUser;
members: Member[];
projectId: number;
roleInfo = roleInfo;
roleInfo = RoleInfo;
private delSub: Subscription;
@ViewChild(AddMemberComponent)
addMemberComponent: AddMemberComponent;
currentUser: SessionUser;
hasProjectAdminRole: boolean;
constructor(private route: ActivatedRoute, private router: Router,
private memberService: MemberService, private messageService: MessageService,
private deletionDialogService: ConfirmationDialogService,
session: SessionService) {
//Get current user from registered resolver.
this.currentUser = session.getCurrentUser();
private session: SessionService) {
this.delSub = deletionDialogService.confirmationConfirm$.subscribe(message => {
if (message &&
message.state === ConfirmationState.CONFIRMED &&
@ -54,7 +57,8 @@ export class MemberComponent implements OnInit, OnDestroy {
.deleteMember(this.projectId, message.data)
.subscribe(
response => {
console.log('Successful change role with user ' + message.data);
this.messageService.announceMessage(response, 'MEMBER.DELETED_SUCCESS', AlertType.SUCCESS);
console.log('Successful delete member: ' + message.data);
this.retrieve(this.projectId, '');
},
error => this.messageService.announceMessage(error.status, 'Failed to change role with user ' + message.data, AlertType.DANGER)
@ -71,8 +75,7 @@ export class MemberComponent implements OnInit, OnDestroy {
error => {
this.router.navigate(['/harbor', 'projects']);
this.messageService.announceMessage(error.status, 'Failed to get project member with project ID:' + projectId, AlertType.DANGER);
}
);
});
}
ngOnDestroy() {
@ -85,6 +88,15 @@ export class MemberComponent implements OnInit, OnDestroy {
//Get projectId from route params snapshot.
this.projectId = +this.route.snapshot.parent.params['id'];
console.log('Get projectId from route params snapshot:' + this.projectId);
this.currentUser = this.session.getCurrentUser();
//Get current user from registered resolver.
let resolverData = this.route.snapshot.parent.data;
if(resolverData) {
this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
}
this.retrieve(this.projectId, '');
}
@ -97,24 +109,27 @@ export class MemberComponent implements OnInit, OnDestroy {
this.retrieve(this.projectId, '');
}
changeRole(userId: number, roleId: number) {
this.memberService
.changeMemberRole(this.projectId, userId, roleId)
.subscribe(
response => {
console.log('Successful change role with user ' + userId + ' to roleId ' + roleId);
this.retrieve(this.projectId, '');
},
error => this.messageService.announceMessage(error.status, 'Failed to change role with user ' + userId + ' to roleId ' + roleId, AlertType.DANGER)
);
changeRole(m: Member, roleId: number) {
if(m) {
this.memberService
.changeMemberRole(this.projectId, m.user_id, roleId)
.subscribe(
response => {
this.messageService.announceMessage(response, 'MEMBER.SWITCHED_SUCCESS', AlertType.SUCCESS);
console.log('Successful change role with user ' + m.user_id + ' to roleId ' + roleId);
this.retrieve(this.projectId, '');
},
error => this.messageService.announceMessage(error.status, 'Failed to change role with user ' + m.user_id + ' to roleId ' + roleId, AlertType.DANGER)
);
}
}
deleteMember(userId: number) {
deleteMember(m: Member) {
let deletionMessage: ConfirmationMessage = new ConfirmationMessage(
'MEMBER.DELETION_TITLE',
'MEMBER.DELETION_SUMMARY',
userId + "",
userId,
m.username,
m.user_id,
ConfirmationTargets.PROJECT_MEMBER
);
this.deletionDialogService.openComfirmDialog(deletionMessage);

View File

@ -6,22 +6,19 @@ import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw';
import { BaseService } from '../../service/base.service';
import { Member } from './member';
@Injectable()
export class MemberService extends BaseService {
export class MemberService {
constructor(private http: Http) {
super();
}
constructor(private http: Http) {}
listMembers(projectId: number, username: string): Observable<Member[]> {
console.log('Get member from project_id:' + projectId + ', username:' + username);
return this.http
.get(`/api/projects/${projectId}/members?username=${username}`)
.map(response=>response.json())
.catch(error=>this.handleError(error));
.map(response=>response.json() as Member[])
.catch(error=>Observable.throw(error));
}
addMember(projectId: number, username: string, roleId: number): Observable<any> {

View File

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

View File

@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Project } from '../project';
import { SessionService } from '../../shared/session.service';
import { ProjectService } from '../../project/project.service';
@Component({
selector: 'project-detail',
@ -13,13 +14,18 @@ import { SessionService } from '../../shared/session.service';
export class ProjectDetailComponent {
currentProject: Project;
isMember: boolean;
constructor(
private route: ActivatedRoute,
private router: Router,
private sessionService: SessionService) {
this.route.data.subscribe(data=>this.currentProject = <Project>data['projectResolver']);
private sessionService: SessionService,
private projectService: ProjectService) {
this.route.data.subscribe(data=>{
this.currentProject = <Project>data['projectResolver'];
this.isMember = this.currentProject.is_member;
});
}
public get isSystemAdmin(): boolean {

View File

@ -3,23 +3,43 @@ import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@a
import { Project } from './project';
import { ProjectService } from './project.service';
import { SessionService } from '../shared/session.service';
import 'rxjs/add/operator/mergeMap';
@Injectable()
export class ProjectRoutingResolver implements Resolve<Project>{
constructor(private projectService: ProjectService, private router: Router) {}
constructor(
private sessionService: SessionService,
private projectService: ProjectService,
private router: Router) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Project> {
let projectId = route.params['id'];
let projectId = route.params['id'];
console.log('Project resolver, projectID:' + projectId);
return this.projectService
.getProject(projectId)
.then(project=> {
if(project) {
return project;
} else {
this.router.navigate(['/harbor', 'projects']);
return null;
}
.toPromise()
.then((project: Project)=> {
if(project) {
let currentUser = this.sessionService.getCurrentUser();
let projectMembers = this.sessionService.getProjectMembers();
if(currentUser && projectMembers) {
let currentMember = projectMembers.find(m=>m.user_id === currentUser.user_id);
if(currentMember) {
project.is_member = true;
project.has_project_admin_role = (currentMember.role_name === 'projectAdmin') || currentUser.has_admin_role === 1;
}
}
return project;
} else {
this.router.navigate(['/harbor', 'projects']);
return null;
}
}).catch(error=>{
this.router.navigate(['/harbor', 'projects']);
return null;
});
}
}

View File

@ -1,11 +1,13 @@
.header-title {
margin-top: 0;
margin-top: 12px;
}
.option-left {
padding-left: 12px;
margin-top: 12px;
padding-left: 12px;
margin-top: 12px;
}
.option-right {
padding-right: 16px;
margin-top: 18px;
}
padding-right: 16px;
margin-top: 18px;
}

View File

@ -1,9 +1,12 @@
<div class="row">
<div class="row" style="margin-right: 12px;">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<h2 class="header-title">{{'PROJECT.PROJECTS' | translate}}</h2>
<div>
<statistics-panel></statistics-panel>
</div>
<div class="row flex-items-xs-between">
<div class="option-left">
<button class="btn btn-primary" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'PROJECT.PROJECT' | translate}}</button>
<button *ngIf="projectCreationRestriction" class="btn btn-primary" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'PROJECT.PROJECT' | translate}}</button>
<create-project (create)="createProject($event)"></create-project>
</div>
<div class="option-right">
@ -18,11 +21,13 @@
</div>
</clr-dropdown>
<grid-filter filterPlaceholder='{{"PROJECT.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchProjects($event)"></grid-filter>
<a href="javascript:void(0)" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></a>
<a href="javascript:void(0)" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</a>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-project [projects]="changedProjects" (toggle)="toggleProject($event)" (delete)="deleteProject($event)" (paginate)="retrieve($event)" [totalPage]="totalPage" [totalRecordCount]="totalRecordCount"></list-project>
</div>
<list-project [projects]="changedProjects" [filteredType]="projectTypes[currentFilteredType]" (toggle)="toggleProject($event)" (delete)="deleteProject($event)" (paginate)="retrieve($event)" [totalPage]="totalPage" [totalRecordCount]="totalRecordCount"></list-project>
</div>
</div>

View File

@ -23,7 +23,9 @@ import { Subscription } from 'rxjs/Subscription';
import { State } from 'clarity-angular';
const types: {} = { 0: 'PROJECT.MY_PROJECTS', 1: 'PROJECT.PUBLIC_PROJECTS' };
import { AppConfigService } from '../app-config.service';
import { SessionService } from '../shared/session.service';
import { ProjectTypes } from '../shared/shared.const';
@Component({
moduleId: module.id,
@ -35,7 +37,7 @@ export class ProjectComponent implements OnInit, OnDestroy {
selected = [];
changedProjects: Project[];
projectTypes = types;
projectTypes = ProjectTypes;
@ViewChild(CreateProjectComponent)
creationProject: CreateProjectComponent;
@ -59,6 +61,8 @@ export class ProjectComponent implements OnInit, OnDestroy {
constructor(
private projectService: ProjectService,
private messageService: MessageService,
private appConfigService: AppConfigService,
private sessionService: SessionService,
private deletionDialogService: ConfirmationDialogService) {
this.subscription = deletionDialogService.confirmationConfirm$.subscribe(message => {
if (message &&
@ -69,6 +73,7 @@ export class ProjectComponent implements OnInit, OnDestroy {
.deleteProject(projectId)
.subscribe(
response => {
this.messageService.announceMessage(response, 'PROJECT.DELETED_SUCCESS', AlertType.SUCCESS);
console.log('Successful delete project with ID:' + projectId);
this.retrieve();
},
@ -76,11 +81,13 @@ export class ProjectComponent implements OnInit, OnDestroy {
);
}
});
}
ngOnInit(): void {
this.projectName = '';
this.isPublic = 0;
}
ngOnDestroy(): void {
@ -89,6 +96,19 @@ export class ProjectComponent implements OnInit, OnDestroy {
}
}
get projectCreationRestriction(): boolean {
let account = this.sessionService.getCurrentUser();
if(account) {
switch(this.appConfigService.getConfig().project_creation_restriction) {
case 'adminonly':
return (account.has_admin_role === 1);
case 'everyone':
return true;
}
}
return false;
}
retrieve(state?: State): void {
if (state) {
this.page = state.page.to + 1;
@ -123,7 +143,7 @@ export class ProjectComponent implements OnInit, OnDestroy {
}
doFilterProjects(filteredType: number): void {
console.log('Filter projects with type:' + types[filteredType]);
console.log('Filter projects with type:' + this.projectTypes[filteredType]);
this.isPublic = filteredType;
this.currentFilteredType = filteredType;
this.retrieve();
@ -135,7 +155,10 @@ export class ProjectComponent implements OnInit, OnDestroy {
this.projectService
.toggleProjectPublic(p.project_id, p.public)
.subscribe(
response => console.log('Successful toggled project_id:' + p.project_id),
response => {
this.messageService.announceMessage(response, 'PROJECT.TOGGLED_SUCCESS', AlertType.SUCCESS);
console.log('Successful toggled project_id:' + p.project_id);
},
error => this.messageService.announceMessage(error.status, error, AlertType.WARNING)
);
}

View File

@ -3,8 +3,6 @@ import { Injectable } from '@angular/core';
import { Http, Headers, RequestOptions, Response, URLSearchParams } from '@angular/http';
import { Project } from './project';
import { BaseService } from '../service/base.service';
import { Message } from '../global-message/message';
import { Observable } from 'rxjs/Observable';
@ -22,11 +20,10 @@ export class ProjectService {
constructor(private http: Http) {}
getProject(projectId: number): Promise<Project> {
getProject(projectId: number): Observable<any> {
return this.http
.get(`/api/projects/${projectId}`)
.toPromise()
.then(response=>response.json() as Project)
.map(response=>response.json())
.catch(error=>Observable.throw(error));
}
@ -70,4 +67,11 @@ export class ProjectService {
.catch(error=>Observable.throw(error));
}
checkProjectMember(projectId: number): Observable<any> {
return this.http
.get(`/api/projects/${projectId}/members`)
.map(response=>response.json())
.catch(error=>Observable.throw(error));
}
}

View File

@ -29,4 +29,6 @@ export class Project {
update_time: Date;
current_user_role_id: number;
repo_count: number;
has_project_admin_role: boolean;
is_member: boolean;
}

View File

@ -108,6 +108,7 @@ export class CreateEditDestinationComponent implements AfterViewChecked {
.createTarget(this.target)
.subscribe(
response=>{
this.messageService.announceMessage(response, 'DESTINATION.CREATED_SUCCESS', AlertType.SUCCESS);
console.log('Successful added target.');
this.createEditDestinationOpened = false;
this.reload.emit(true);
@ -129,7 +130,7 @@ export class CreateEditDestinationComponent implements AfterViewChecked {
.get(errorMessageKey)
.subscribe(res=>{
this.messageService.announceMessage(error.status, errorMessageKey, AlertType.DANGER);
this.inlineAlert.showInlineError(errorMessageKey);
this.inlineAlert.showInlineError(res);
});
}
);
@ -139,6 +140,7 @@ export class CreateEditDestinationComponent implements AfterViewChecked {
.updateTarget(this.target)
.subscribe(
response=>{
this.messageService.announceMessage(response, 'DESTINATION.UPDATED_SUCCESS', AlertType.SUCCESS);
console.log('Successful updated target.');
this.createEditDestinationOpened = false;
this.reload.emit(true);
@ -158,7 +160,7 @@ export class CreateEditDestinationComponent implements AfterViewChecked {
this.translateService
.get(errorMessageKey)
.subscribe(res=>{
this.inlineAlert.showInlineError(errorMessageKey);
this.inlineAlert.showInlineError(res);
this.messageService.announceMessage(error.status, errorMessageKey, AlertType.DANGER);
});
}

View File

@ -43,14 +43,15 @@ export class DestinationComponent implements OnInit {
.deleteTarget(targetId)
.subscribe(
response => {
this.messageService.announceMessage(response, 'DESTINATION.DELETED_SUCCESS', AlertType.SUCCESS);
console.log('Successful deleted target with ID:' + targetId);
this.reload();
},
error => this.messageService
.announceMessage(error.status,
'Failed to delete target with ID:' + targetId + ', error:' + error,
AlertType.DANGER)
);
error => {
this.messageService
.announceMessage(error.status,'DESTINATION.DELETED_FAILED', AlertType.DANGER);
console.log('Failed to delete target with ID:' + targetId + ', error:' + error);
});
}
});
}

View File

@ -1,8 +1,6 @@
import { Injectable } from '@angular/core';
import { Http, URLSearchParams, Response } from '@angular/http';
import { BaseService } from '../service/base.service';
import { Policy } from './policy';
import { Job } from './job';
import { Target } from './target';
@ -14,10 +12,8 @@ import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/mergeMap';
@Injectable()
export class ReplicationService extends BaseService {
constructor(private http: Http) {
super();
}
export class ReplicationService {
constructor(private http: Http) {}
listPolicies(policyName: string, projectId?: any): Observable<Policy[]> {
if(!projectId) {

View File

@ -3,7 +3,7 @@
<clr-dg-column>{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let r of repositories" [clrDgItem]='r'>
<clr-dg-action-overflow *ngIf="listFullMode">
<clr-dg-action-overflow *ngIf="listFullMode && hasProjectAdminRole">
<button class="action-item">{{'REPOSITORY.COPY_ID' | translate}}</button>
<button class="action-item">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</button>
<button class="action-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</button>

View File

@ -1,4 +1,4 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { Router, NavigationExtras } from '@angular/router';
import { Repository } from '../repository';
import { State } from 'clarity-angular';
@ -7,14 +7,18 @@ import { SearchTriggerService } from '../../base/global-search/search-trigger.se
import { SessionService } from '../../shared/session.service';
import { ListMode } from '../../shared/shared.const';
import { SessionUser } from '../../shared/session-user';
@Component({
selector: 'list-repository',
templateUrl: 'list-repository.component.html'
})
export class ListRepositoryComponent {
export class ListRepositoryComponent implements OnInit {
@Input() projectId: number;
@Input() repositories: Repository[];
@Output() delete = new EventEmitter<string>();
@Input() totalPage: number;
@ -22,6 +26,7 @@ export class ListRepositoryComponent {
@Output() paginate = new EventEmitter<State>();
@Input() mode: string = ListMode.FULL;
@Input() hasProjectAdminRole: boolean;
pageOffset: number = 1;
@ -30,6 +35,8 @@ export class ListRepositoryComponent {
private searchTrigger: SearchTriggerService,
private session: SessionService) { }
ngOnInit() {}
deleteRepo(repoName: string) {
this.delete.emit(repoName);
}

View File

@ -8,6 +8,6 @@
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-repository [projectId]="projectId" [repositories]="changedRepositories" (delete)="deleteRepo($event)" [totalPage]="totalPage" [totalRecordCount]="totalRecordCount" (paginate)="retrieve($event)"></list-repository>
<list-repository [projectId]="projectId" [repositories]="changedRepositories" (delete)="deleteRepo($event)" [totalPage]="totalPage" [totalRecordCount]="totalRecordCount" [hasProjectAdminRole]="hasProjectAdminRole" (paginate)="retrieve($event)"></list-repository>
</div>
</div>

View File

@ -14,6 +14,8 @@ import { Subscription } from 'rxjs/Subscription';
import { State } from 'clarity-angular';
import { Project } from '../project/project';
const repositoryTypes = [
{ key: '0', description: 'REPOSITORY.MY_REPOSITORY' },
{ key: '1', description: 'REPOSITORY.PUBLIC_REPOSITORY' }
@ -39,6 +41,8 @@ export class RepositoryComponent implements OnInit {
totalPage: number;
totalRecordCount: number;
hasProjectAdminRole: boolean;
subscription: Subscription;
constructor(
@ -60,17 +64,22 @@ export class RepositoryComponent implements OnInit {
.subscribe(
response => {
this.refresh();
this.messageService.announceMessage(response, 'REPOSITORY.DELETED_REPO_SUCCESS', AlertType.SUCCESS);
console.log('Successful deleted repo:' + repoName);
},
error => this.messageService.announceMessage(error.status, 'Failed to delete repo:' + repoName, AlertType.DANGER)
);
}
}
);
});
}
ngOnInit(): void {
this.projectId = this.route.snapshot.parent.params['id'];
let resolverData = this.route.snapshot.parent.data;
if(resolverData) {
this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
}
this.currentRepositoryType = this.repositoryTypes[0];
this.lastFilteredRepoName = '';
this.retrieve();

View File

@ -3,19 +3,19 @@
<clr-datagrid>
<clr-dg-column>{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.OS' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let t of tags" [clrDgItem]='t'>
<clr-dg-action-overflow>
<clr-dg-action-overflow *ngIf="hasProjectAdminRole">
<button class="action-item" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{t.tag}}</clr-dg-cell>
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell>
<clr-dg-cell>
<clr-dg-cell *ngIf="withNotary">
<clr-icon shape="check" *ngIf="t.signed" style="color: #1D5100;"></clr-icon>
<clr-icon shape="close" *ngIf="!t.signed" style="color: #C92100;"></clr-icon>
</clr-dg-cell>

View File

@ -10,10 +10,15 @@ import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmati
import { Subscription } from 'rxjs/Subscription';
import { Tag } from '../tag';
import { TagView } from '../tag-view';
import { AppConfigService } from '../../app-config.service';
import { SessionService } from '../../shared/session.service';
import { Project } from '../../project/project';
@Component({
moduleId: module.id,
selector: 'tag-repository',
@ -25,8 +30,11 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
projectId: number;
repoName: string;
hasProjectAdminRole: boolean = false;
tags: TagView[];
registryUrl: string;
withNotary: boolean;
private subscription: Subscription;
@ -35,7 +43,9 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
private messageService: MessageService,
private deletionDialogService: ConfirmationDialogService,
private repositoryService: RepositoryService,
private appConfigService: AppConfigService) {
private appConfigService: AppConfigService,
private session: SessionService){
this.subscription = this.deletionDialogService.confirmationConfirm$.subscribe(
message => {
if (message &&
@ -52,6 +62,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
.subscribe(
response => {
this.retrieve();
this.messageService.announceMessage(response, 'REPOSITORY.DELETED_TAG_SUCCESS', AlertType.SUCCESS);
console.log('Deleted repo:' + this.repoName + ' with tag:' + tagName);
},
error => this.messageService.announceMessage(error.status, 'Failed to delete tag:' + tagName + ' under repo:' + this.repoName, AlertType.DANGER)
@ -59,15 +70,20 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
}
}
}
}
)
});
}
ngOnInit() {
let resolverData = this.route.snapshot.data;
console.log(JSON.stringify(resolverData));
if(resolverData) {
this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
}
this.projectId = this.route.snapshot.params['id'];
this.repoName = this.route.snapshot.params['repo'];
this.tags = [];
this.registryUrl = this.appConfigService.getConfig().registry_url;
this.withNotary = this.appConfigService.getConfig().with_notary;
this.retrieve();
}
@ -79,25 +95,35 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
retrieve() {
this.tags = [];
this.repositoryService
.listTagsWithVerifiedSignatures(this.repoName)
.subscribe(
items => {
items.forEach(t => {
let tag = new TagView();
tag.tag = t.tag;
let data = JSON.parse(t.manifest.history[0].v1Compatibility);
tag.architecture = data['architecture'];
tag.author = data['author'];
tag.signed = t.signed;
tag.created = data['created'];
tag.dockerVersion = data['docker_version'];
tag.pullCommand = 'docker pull ' + this.registryUrl + '/' + t.manifest.name + ':' + t.tag;
tag.os = data['os'];
this.tags.push(tag);
});
},
error => this.messageService.announceMessage(error.status, 'Failed to list tags with repo:' + this.repoName, AlertType.DANGER));
if(this.withNotary) {
this.repositoryService
.listTagsWithVerifiedSignatures(this.repoName)
.subscribe(
items => this.listTags(items),
error => this.messageService.announceMessage(error.status, 'Failed to list tags with repo:' + this.repoName, AlertType.DANGER));
} else {
this.repositoryService
.listTags(this.repoName)
.subscribe(
items => this.listTags(items),
error => this.messageService.announceMessage(error.status, 'Failed to list tags with repo:' + this.repoName, AlertType.DANGER));
}
}
private listTags(tags: Tag[]): void {
tags.forEach(t => {
let tag = new TagView();
tag.tag = t.tag;
let data = JSON.parse(t.manifest.history[0].v1Compatibility);
tag.architecture = data['architecture'];
tag.author = data['author'];
tag.signed = t.signed;
tag.created = data['created'];
tag.dockerVersion = data['docker_version'];
tag.pullCommand = 'docker pull ' + this.registryUrl + '/' + t.manifest.name + ':' + t.tag;
tag.os = data['os'];
this.tags.push(tag);
});
}
deleteTag(tag: TagView) {

View File

@ -1,8 +0,0 @@
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
export class AuthGuard implements CanActivate {
canActivate() {
return true;
}
}

View File

@ -1,18 +0,0 @@
import { Http, Response,} from '@angular/http';
export class BaseService {
protected handleError(error: Response | any): Promise<any> {
// In a real world app, we might use a remote logging infrastructure
let errMsg: string;
console.log(typeof error);
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
return Promise.reject(error);
}
}

View File

@ -183,6 +183,7 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
.createPolicy(this.getPolicyByForm())
.subscribe(
response=>{
this.messageService.announceMessage(response, 'REPLICATION.CREATED_SUCCESS', AlertType.SUCCESS);
console.log('Successful created policy: ' + response);
this.createEditPolicyOpened = false;
this.reload.emit(true);
@ -199,6 +200,7 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
.createOrUpdatePolicyWithNewTarget(this.getPolicyByForm(), this.getTargetByForm())
.subscribe(
response=>{
this.messageService.announceMessage(response, 'REPLICATION.UPDATED_SUCCESS', AlertType.SUCCESS);
console.log('Successful created policy and target:' + response);
this.createEditPolicyOpened = false;
this.reload.emit(true);

View File

@ -50,7 +50,10 @@ export class ListPolicyComponent implements OnDestroy {
this.replicationService
.enablePolicy(policy.id, policy.enabled)
.subscribe(
res => console.log('Successful toggled policy status'),
response => {
this.messageService.announceMessage(response, 'REPLICATION.TOGGLED_SUCCESS', AlertType.SUCCESS);
console.log('Successful toggled policy status')
},
error => this.messageService.announceMessage(error.status, "Failed to toggle policy status.", AlertType.DANGER)
);
}
@ -67,10 +70,11 @@ export class ListPolicyComponent implements OnDestroy {
.deletePolicy(message.data)
.subscribe(
response => {
this.messageService.announceMessage(response, 'REPLICATION.DELETED_SUCCESS', AlertType.SUCCESS);
console.log('Successful delete policy with ID:' + message.data);
this.reload.emit(true);
},
error => this.messageService.announceMessage(error.status, 'Failed to delete policy with ID:' + message.data, AlertType.DANGER)
error => this.messageService.announceMessage(error.status, 'REPLICATION.DELETED_FAILED', AlertType.DANGER)
);
}
}

View File

@ -0,0 +1,43 @@
import { Injectable } from '@angular/core';
import {
CanActivate, Router,
ActivatedRouteSnapshot,
RouterStateSnapshot,
CanActivateChild
} from '@angular/router';
import { SessionService } from '../../shared/session.service';
import { ProjectService } from '../../project/project.service';
import { CommonRoutes } from '../../shared/shared.const';
@Injectable()
export class MemberGuard implements CanActivate, CanActivateChild {
constructor(
private sessionService: SessionService,
private projectService: ProjectService,
private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> | boolean {
let projectId = route.params['id'];
this.sessionService.setProjectMembers([]);
return new Promise((resolve, reject) => {
this.projectService.checkProjectMember(projectId)
.subscribe(
res=>{
this.sessionService.setProjectMembers(res);
return resolve(true)
},
error => {
//Add exception for repository in project detail router activation.
if(state.url.endsWith('repository')) {
return resolve(true);
}
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
return resolve(false);
});
});
}
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> | boolean {
return this.canActivate(route, state);
}
}

View File

@ -3,6 +3,8 @@ import { Headers, Http, URLSearchParams } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { SessionUser } from './session-user';
import { Member } from '../project/member/member';
import { SignInCredential } from './sign-in-credential';
import { enLang } from '../shared/shared.const'
@ -27,6 +29,8 @@ const langMap = {
export class SessionService {
currentUser: SessionUser = null;
projectMembers: Member[];
private headers = new Headers({
"Content-Type": 'application/json'
});
@ -143,4 +147,12 @@ export class SessionService {
})
.catch(error => this.handleError(error));
}
setProjectMembers(projectMembers: Member[]): void {
this.projectMembers = projectMembers;
}
getProjectMembers(): Member[] {
return this.projectMembers;
}
}

View File

@ -24,7 +24,8 @@ export const enum ConfirmationTargets {
REPOSITORY,
TAG,
CONFIG,
CONFIG_ROUTE
CONFIG_ROUTE,
CONFIG_TAB
};
export const enum ActionType {
@ -51,4 +52,7 @@ export const CookieKeyOfAdmiral = "admiral.endpoint.latest";
export const enum ConfirmationState {
NA, CONFIRMED, CANCEL
}
}
export const ProjectTypes = { 0: 'PROJECT.MY_PROJECTS', 1: 'PROJECT.PUBLIC_PROJECTS' };
export const RoleInfo = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER', 3: 'MEMBER.GUEST' };

View File

@ -32,6 +32,7 @@ import { StatisticsComponent } from './statictics/statistics.component';
import { StatisticsPanelComponent } from './statictics/statistics-panel.component';
import { SignInGuard } from './route/sign-in-guard-activate.service';
import { LeavingConfigRouteDeactivate } from './route/leaving-config-deactivate.service';
import { MemberGuard } from './route/member-guard-activate.service';
@NgModule({
imports: [
@ -79,7 +80,8 @@ import { LeavingConfigRouteDeactivate } from './route/leaving-config-deactivate.
SystemAdminGuard,
AuthCheckGuard,
SignInGuard,
LeavingConfigRouteDeactivate
LeavingConfigRouteDeactivate,
MemberGuard
]
})
export class SharedModule {

View File

@ -1,25 +1,41 @@
<div class="card card-block">
<h3 class="card-title">{{'STATISTICS.TITLE' | translate }}</h3>
<span class="card-text">
<div class="row">
<div class="col-xs-2 col-sm-2 col-md-2 col-lg-2 col-xl-2">
<span class="statistic-column-title">{{'STATISTICS.PRO_ITEM' | translate }}</span>
</div>
<div class="col-xs-10 col-sm-10 col-md-10 col-lg-10 col-xl-10">
<statistics [data]='{number: originalCopy.my_project_count, label: "my"}'></statistics>
<statistics [data]='{number: originalCopy.public_project_count, label: "pub"}'></statistics>
<statistics [data]='{number: originalCopy.total_project_count, label: "total"}' *ngIf="isValidSession"></statistics>
</div>
</div>
<div class="row">
<div class="col-xs-2 col-sm-2 col-md-2 col-lg-2 col-xl-2">
<span class="statistic-column-title">{{'STATISTICS.REPO_ITEM' | translate }}</span>
<div class="row flex-items-xs-between flex-items-xs-middle">
<div></div>
<div id="right_statistic_panel" style="margin-right: 18px;">
<div class="statistic-block">
<div class="statistic-column-block">
<div>
<span class="statistic-column-title statistic-column-title-pro">{{'STATISTICS.PRO_ITEM' | translate }}</span>
</div>
<div>
<span class="statistic-column-title statistic-column-title-repo">{{'STATISTICS.REPO_ITEM' | translate }}</span>
</div>
</div>
<div class="statistic-column-block" style="margin-left: 16px;">
<div>
<statistics [data]='originalCopy.my_project_count' [label]='"STATISTICS.INDEX_MY" | translate'></statistics>
</div>
<div>
<statistics [data]='originalCopy.my_repo_count' [label]='"STATISTICS.INDEX_MY" | translate'></statistics>
</div>
</div>
<div class="statistic-column-block" style="margin-left: 28px;">
<div>
<statistics [data]='originalCopy.public_project_count' [label]='"STATISTICS.INDEX_PUB" | translate'></statistics>
</div>
<div>
<statistics [data]='originalCopy.public_repo_count' [label]='"STATISTICS.INDEX_PUB" | translate'></statistics>
</div>
</div>
<div class="statistic-column-block" style="margin-left: 28px;">
<div>
<statistics [data]='originalCopy.total_project_count' [label]='"STATISTICS.INDEX_TOTAL" | translate' *ngIf="isValidSession"></statistics>
</div>
<div>
<statistics [data]='originalCopy.total_repo_count' [label]='"STATISTICS.INDEX_TOTAL" | translate' *ngIf="isValidSession"></statistics>
</div>
</div>
</div>
<div class="statistic-item-divider"></div>
<div class="statistic-block">Storage</div>
</div>
<div class="col-xs-10 col-sm-10 col-md-10 col-lg-10 col-xl-10">
<statistics [data]='{number: originalCopy.my_repo_count, label: "my"}'></statistics>
<statistics [data]='{number: originalCopy.public_repo_count, label: "pub"}'></statistics>
<statistics [data]='{number: originalCopy.total_repo_count, label: "total"}' *ngIf="isValidSession"></statistics>
</div>
</div>
</span>
</div>

View File

@ -1,30 +1,57 @@
.statistic-wrapper {
padding: 12px;
margin: 12px;
text-align: center;
padding: 4px;
margin: 4px;
text-align: right;
vertical-align: middle;
height: 72px;
min-width: 108px;
max-width: 216px;
height: 30px;
display: inline-block;
}
.statistic-data {
font-size: 48px;
font-weight: bolder;
font-family: "Metropolis";
line-height: 48px;
font-size: 16px;
font-weight: 900;
font-family: "semibold";
line-height: 16px;
}
.statistic-text {
font-size: 24px;
font-weight: 400;
line-height: 24px;
font-size: 10px;
line-height: 10px;
text-transform: uppercase;
font-family: "Metropolis";
font-family: "semibold";
}
.statistic-column-block {
display: inline-block;
text-align: right;
}
.statistic-column-title {
position: relative;
top: 40%;
text-transform: uppercase;
font-size: 14px;
}
.statistic-column-title-pro {
top: -10px;
}
.statistic-column-title-repo {
top: 3px;
}
.statistic-item-divider {
height: 54px;
display: inline-block;
width: 1px;
background-color: #ccc;
opacity: 0.55;
margin-left: 4px;
margin-right: 12px;
position: relative;
top: 3px;
}
.statistic-block {
display: inline-block;
}

View File

@ -1,4 +1,4 @@
<div class="statistic-wrapper">
<span class="statistic-data">{{data.number}}</span>
<span class="statistic-text">{{data.label}}</span>
<span class="statistic-data">{{data}}</span>
<span class="statistic-text">{{label}}</span>
</div>

View File

@ -7,5 +7,6 @@ import { Component, Input } from '@angular/core';
})
export class StatisticsComponent {
@Input() data: any;
@Input() label: string;
@Input() data: number = 0;
}

View File

@ -110,10 +110,10 @@
"PROJECT": {
"PROJECTS": "Projects",
"NAME": "Project Name",
"ROLE": "Role",
"PUBLIC_OR_PRIVATE": "Public",
"REPO_COUNT": "Repositories Count",
"CREATION_TIME": "Creation Time",
"DESCRIPTION": "Description",
"PUBLIC": "Public",
"PRIVATE": "Private",
"MAKE": "Make",
@ -132,7 +132,10 @@
"DELETION_TITLE": "Confirm project deletion",
"DELETION_SUMMARY": "Do you want to delete project {{param}}?",
"FILTER_PLACEHOLDER": "Filter Projects",
"REPLICATION_RULE": "Replication Rule"
"REPLICATION_RULE": "Replication Rule",
"CREATED_SUCCESS": "Created project successfully.",
"DELETED_SUCCESS": "Deleted project successfully.",
"TOGGLED_SUCCESS": "Toggled project successfully."
},
"PROJECT_DETAIL": {
"REPOSITORIES": "Repositories",
@ -159,7 +162,10 @@
"UNKNOWN_ERROR": "Unknown error occurred while adding member.",
"FILTER_PLACEHOLDER": "Filter Members",
"DELETION_TITLE": "Confirm project member deletion",
"DELETION_SUMMARY": "Do you want to delete project member {{param}}?"
"DELETION_SUMMARY": "Do you want to delete project member {{param}}?",
"ADDED_SUCCESS": "Added member successfully.",
"DELETED_SUCCESS": "Deleted member successfully.",
"SWITCHED_SUCCESS": "Switched member role successfully."
},
"AUDIT_LOG": {
"USERNAME": "Username",
@ -235,7 +241,12 @@
"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."
"CONFIRM_TOGGLE_DISABLE_POLICY": "After disabling the policy, all unfinished replication jobs of this policy will be stopped and canceled. Please confirm to continue.",
"CREATED_SUCCESS": "Created policy successfully.",
"UPDATED_SUCCESS": "Updated policy successfully.",
"DELETED_SUCCESS": "Deleted policy successfully.",
"DELETED_FAILED": "Deleted policy failed.",
"TOGGLED_SUCCESS": "Toggled policy status successfully."
},
"DESTINATION": {
"NEW_ENDPOINT": "New Endpoint",
@ -257,7 +268,11 @@
"INVALID_NAME": "Invalid destination name.",
"FAILED_TO_GET_TARGET": "Failed to get endpoint.",
"CREATION_TIME": "Creation Time",
"ITEMS": "item(s)"
"ITEMS": "item(s)",
"CREATED_SUCCESS": "Created destination successfully.",
"UPDATED_SUCCESS": "Updated destination successfully.",
"DELETED_SUCCESS": "Deleted destination successfully.",
"DELETED_FAILED": "Deleted destination failed."
},
"REPOSITORY": {
"COPY_ID": "Copy ID",
@ -275,7 +290,7 @@
"DELETION_SUMMARY_TAG": "Do you want to delete tag {{param}}?",
"DELETION_TITLE_TAG_DENIED": "Signed Tag can't be deleted",
"DELETION_SUMMARY_TAG_DENIED": "The tag must be removed from the Notary before it can be deleted. {{param}}",
"FILTER_FOR_REPOSITORIES": "Filter for repositories",
"FILTER_FOR_REPOSITORIES": "Filter Repositories",
"TAG": "Tag",
"SIGNED": "Signed",
"AUTHOR": "Author",
@ -286,7 +301,9 @@
"SHOW_DETAILS": "Show Details",
"REPOSITORIES": "Repositories",
"ITEMS": "item(s)",
"POP_REPOS": "Popular Repositories"
"POP_REPOS": "Popular Repositories",
"DELETED_REPO_SUCCESS": "Deleted repository successfully.",
"DELETED_TAG_SUCCESS": "Deleted tag successfully."
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet, do you really want to cancel?"
@ -310,7 +327,7 @@
"EMAIL": "Email",
"SYSTEM": "System Settings",
"CONFIRM_TITLE": "Confirm to cancel",
"CONFIRM_SUMMARY": "Some changes are not saved yet, do you really want to leave?",
"CONFIRM_SUMMARY": "Some changes are not saved yet, do you really want to discard?",
"SAVE_SUCCESS": "Configurations have been successfully saved",
"MAIL_SERVER": "Email Server",
"MAIL_SERVER_PORT": "Email Server Port",
@ -386,7 +403,8 @@
"IN_PROGRESS": "Search...",
"BACK": "Back"
},
"UNKNOWN_ERROR": "Some unknown errors HAVE occurred. Please try again later",
"UNKNOWN_ERROR": "Unknown errors have occurred. Please try again later",
"UNAUTHORIZED_ERROR": "Your session is invalid or has expired. You need to sign in to continue the operation",
"FORBIDDEN_ERROR": "You are not allowed to perform this operation"
"FORBIDDEN_ERROR": "You are not allowed to perform this operation",
"GENERAL_ERROR": "Errors have occurred when performing service call: {{param}}"
}

View File

@ -110,10 +110,10 @@
"PROJECT": {
"PROJECTS": "项目",
"NAME": "项目名称",
"ROLE": "角色",
"PUBLIC_OR_PRIVATE": "公开",
"REPO_COUNT": "镜像仓库数",
"CREATION_TIME": "创建时间",
"DESCRIPTION": "描述",
"PUBLIC": "公开",
"PRIVATE": "私有",
"MAKE": "设为",
@ -132,7 +132,10 @@
"DELETION_TITLE": "删除项目确认",
"DELETION_SUMMARY": "你确认删除项目 {{param}}",
"FILTER_PLACEHOLDER": "过滤项目",
"REPLICATION_RULE": "复制策略"
"REPLICATION_RULE": "复制策略",
"CREATED_SUCCESS": "创建项目成功。",
"DELETED_SUCCESS": "删除项目成功。",
"TOGGLED_SUCCESS": "切换状态成功。"
},
"PROJECT_DETAIL": {
"REPOSITORIES": "镜像仓库",
@ -159,7 +162,10 @@
"UNKNOWN_ERROR": "添加成员时发生未知错误。",
"FILTER_PLACEHOLDER": "过滤成员",
"DELETION_TITLE": "删除项目成员确认",
"DELETION_SUMMARY": "你确认删除项目成员 {{param}}?"
"DELETION_SUMMARY": "你确认删除项目成员 {{param}}?",
"ADDED_SUCCESS": "新增成员成功。",
"DELETED_SUCCESS": "删除成员成功",
"SWITCHED_SUCCESS": "切换角色成功"
},
"AUDIT_LOG": {
"USERNAME": "用户名",
@ -235,7 +241,12 @@
"TOGGLE_ENABLE_TITLE": "启用策略",
"CONFIRM_TOGGLE_ENABLE_POLICY": "启用策略后,该项目下的所有镜像仓库将复制到目标实例。请确认继续。",
"TOGGLE_DISABLE_TITLE": "停用策略",
"CONFIRM_TOGGLE_DISABLE_POLICY": "停用策略后,所有未完成的复制任务将被终止和取消。请确认继续。"
"CONFIRM_TOGGLE_DISABLE_POLICY": "停用策略后,所有未完成的复制任务将被终止和取消。请确认继续。",
"CREATED_SUCCESS": "创建复制策略成功。",
"UPDATED_SUCCESS": "更新复制策略成功。",
"DELETED_SUCCESS": "删除复制策略成功。",
"DELETED_FAILED": "删除复制策略失败。",
"TOGGLED_SUCCESS": "切换复制策略状态成功。"
},
"DESTINATION": {
"NEW_ENDPOINT": "新建目标",
@ -257,7 +268,11 @@
"INVALID_NAME": "无效的目标名称。",
"FAILED_TO_GET_TARGET": "获取目标失败。",
"CREATION_TIME": "创建时间",
"ITEMS": "条记录"
"ITEMS": "条记录",
"CREATED_SUCCESS": "创建目标成功。",
"UPDATED_SUCCESS": "更新目标成功。",
"DELETED_SUCCESS": "删除目标成功。",
"DELETED_FAILED": "删除目标失败。"
},
"REPOSITORY": {
"COPY_ID": "复制ID",
@ -286,7 +301,9 @@
"SHOW_DETAILS": "显示详细",
"REPOSITORIES": "镜像仓库",
"ITEMS": "条记录",
"POP_REPOS": "受欢迎的镜像库"
"POP_REPOS": "受欢迎的镜像库",
"DELETED_REPO_SUCCESS": "删除镜像仓库成功。",
"DELETED_TAG_SUCCESS": "删除镜像标签成功。"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "表单内容改变,确认取消?"
@ -378,8 +395,8 @@
"TITLE": "统计",
"PRO_ITEM": "项目",
"REPO_ITEM": "镜像库",
"INDEX_MY": "私有",
"INDEX_PUB": "公开",
"INDEX_MY": "私有",
"INDEX_PUB": "公开",
"INDEX_TOTAL": "总计"
},
"SEARCH": {
@ -388,5 +405,6 @@
},
"UNKNOWN_ERROR": "发生未知错误,请稍后再试",
"UNAUTHORIZED_ERROR": "会话无效或者已经过期, 请重新登录以继续",
"FORBIDDEN_ERROR": "当前操作被禁止,请确认你有合法的权限"
"FORBIDDEN_ERROR": "当前操作被禁止,请确认你有合法的权限",
"GENERAL_ERROR": "调用后台服务时出现错误: {{param}}"
}

View File

@ -1,49 +0,0 @@
// Fetch prints the content found at a URL.
package main
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"time"
)
func main() {
time.Sleep(60 * time.Second)
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
var client = &http.Client{
Timeout: time.Second * 30,
Transport: tr,
}
for _, url := range os.Args[1:] {
resp, err := client.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
os.Exit(1)
}
// fmt.Printf("%s", b)
if strings.Contains(string(b), "Harbor") {
fmt.Printf("sucess!\n")
} else {
fmt.Println("the response does not contain \"Harbor\"!")
fmt.Println(string(b))
os.Exit(1)
}
}
}

28
tests/startuptest.sh Executable file
View File

@ -0,0 +1,28 @@
#!/bin/sh
set +e
TIMEOUT=12
while [ $TIMEOUT -gt 0 ]; do
STATUS=$(curl --insecure -s -o /dev/null -w '%{http_code}' https://localhost/)
if [ $STATUS -eq 200 ]; then
break
fi
TIMEOUT=$(($TIMEOUT - 1))
sleep 5
done
if [ $TIMEOUT -eq 0 ]; then
echo "Harbor cannot reach within one minute."
exit 1
fi
curl --insecure -s -L -H "Accept: application/json" https://localhost/ | grep "Harbor" > /dev/null
if [ $? -eq 0 ]; then
echo "Harbor is running success."
else
echo "Harbor is running fail."
exit 1
fi