mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-22 02:05:41 +01:00
remove chartmuseum backend (#18191)
Harbor deprecates chartmuseum as of v2.8.0 Epic: https://github.com/goharbor/harbor/issues/17958 Discussion: https://github.com/goharbor/harbor/discussions/15057 Signed-off-by: Wang Yan <wangyan@vmware.com>
This commit is contained in:
parent
ec5afc3257
commit
738fde7d3b
4
.github/workflows/build-package.yml
vendored
4
.github/workflows/build-package.yml
vendored
@ -88,8 +88,8 @@ jobs:
|
||||
else
|
||||
build_base_params=" BUILD_BASE=true PUSHBASEIMAGE=true REGISTRYUSER=\"${{ secrets.DOCKER_HUB_USERNAME }}\" REGISTRYPASSWORD=\"${{ secrets.DOCKER_HUB_PASSWORD }}\""
|
||||
fi
|
||||
sudo make package_offline GOBUILDTAGS="include_oss include_gcs" BASEIMAGETAG=${Harbor_Build_Base_Tag} VERSIONTAG=${Harbor_Assets_Version} PKGVERSIONTAG=${Harbor_Package_Version} NOTARYFLAG=true CHARTFLAG=true TRIVYFLAG=true HTTPPROXY= ${build_base_params}
|
||||
sudo make package_online GOBUILDTAGS="include_oss include_gcs" BASEIMAGETAG=${Harbor_Build_Base_Tag} VERSIONTAG=${Harbor_Assets_Version} PKGVERSIONTAG=${Harbor_Package_Version} NOTARYFLAG=true CHARTFLAG=true TRIVYFLAG=true HTTPPROXY= ${build_base_params}
|
||||
sudo make package_offline GOBUILDTAGS="include_oss include_gcs" BASEIMAGETAG=${Harbor_Build_Base_Tag} VERSIONTAG=${Harbor_Assets_Version} PKGVERSIONTAG=${Harbor_Package_Version} NOTARYFLAG=true TRIVYFLAG=true HTTPPROXY= ${build_base_params}
|
||||
sudo make package_online GOBUILDTAGS="include_oss include_gcs" BASEIMAGETAG=${Harbor_Build_Base_Tag} VERSIONTAG=${Harbor_Assets_Version} PKGVERSIONTAG=${Harbor_Package_Version} NOTARYFLAG=true TRIVYFLAG=true HTTPPROXY= ${build_base_params}
|
||||
harbor_offline_build_bundle=$(basename harbor-offline-installer-*.tgz)
|
||||
harbor_online_build_bundle=$(basename harbor-online-installer-*.tgz)
|
||||
echo "Package name is: $harbor_offline_build_bundle"
|
||||
|
24
Makefile
24
Makefile
@ -82,8 +82,6 @@ TRIVYFLAG=false
|
||||
HTTPPROXY=
|
||||
BUILDBIN=true
|
||||
NPM_REGISTRY=https://registry.npmjs.org
|
||||
# enable/disable chart repo supporting
|
||||
CHARTFLAG=false
|
||||
BUILDTARGET=build
|
||||
GEN_TLS=
|
||||
|
||||
@ -94,7 +92,7 @@ VERSIONTAG=dev
|
||||
BUILD_BASE=true
|
||||
PUSHBASEIMAGE=false
|
||||
BASEIMAGETAG=dev
|
||||
BUILDBASETARGET=chartserver trivy-adapter core db jobservice log nginx notary-server notary-signer portal prepare redis registry registryctl exporter
|
||||
BUILDBASETARGET=trivy-adapter core db jobservice log nginx notary-server notary-signer portal prepare redis registry registryctl exporter
|
||||
IMAGENAMESPACE=goharbor
|
||||
BASEIMAGENAMESPACE=goharbor
|
||||
# #input true/false only
|
||||
@ -112,17 +110,10 @@ NOTARYMIGRATEVERSION=v4.11.0
|
||||
TRIVYVERSION=v0.37.2
|
||||
TRIVYADAPTERVERSION=v0.30.7
|
||||
|
||||
# version of chartmuseum for pulling the source code
|
||||
CHARTMUSEUM_SRC_TAG=v0.14.0
|
||||
|
||||
# version of chartmuseum
|
||||
CHARTMUSEUMVERSION=$(CHARTMUSEUM_SRC_TAG)-redis
|
||||
|
||||
# version of registry for pulling the source code
|
||||
REGISTRY_SRC_TAG=v2.8.0
|
||||
|
||||
# dependency binaries
|
||||
CHARTURL=https://storage.googleapis.com/harbor-builds/bin/chartmuseum/release-${CHARTMUSEUMVERSION}/chartm
|
||||
NOTARYURL=https://storage.googleapis.com/harbor-builds/bin/notary/release-${NOTARYVERSION}/binary-bundle.tgz
|
||||
REGISTRYURL=https://storage.googleapis.com/harbor-builds/bin/registry/release-${REGISTRYVERSION}/registry
|
||||
TRIVY_DOWNLOAD_URL=https://github.com/aquasecurity/trivy/releases/download/$(TRIVYVERSION)/trivy_$(TRIVYVERSION:v%=%)_Linux-64bit.tar.gz
|
||||
@ -134,7 +125,6 @@ REGISTRY_VERSION: $(REGISTRYVERSION)
|
||||
NOTARY_VERSION: $(NOTARYVERSION)
|
||||
TRIVY_VERSION: $(TRIVYVERSION)
|
||||
TRIVY_ADAPTER_VERSION: $(TRIVYADAPTERVERSION)
|
||||
CHARTMUSEUM_VERSION: $(CHARTMUSEUMVERSION)
|
||||
endef
|
||||
|
||||
# docker parameters
|
||||
@ -216,10 +206,6 @@ endif
|
||||
ifeq ($(TRIVYFLAG), true)
|
||||
PREPARECMD_PARA+= --with-trivy
|
||||
endif
|
||||
# append chartmuseum parameters if set
|
||||
ifeq ($(CHARTFLAG), true)
|
||||
PREPARECMD_PARA+= --with-chartmuseum
|
||||
endif
|
||||
|
||||
# makefile
|
||||
MAKEFILEPATH_PHOTON=$(MAKEPATH)/photon
|
||||
@ -234,7 +220,6 @@ DOCKERIMAGENAME_CORE=$(IMAGENAMESPACE)/harbor-core
|
||||
DOCKERIMAGENAME_JOBSERVICE=$(IMAGENAMESPACE)/harbor-jobservice
|
||||
DOCKERIMAGENAME_LOG=$(IMAGENAMESPACE)/harbor-log
|
||||
DOCKERIMAGENAME_DB=$(IMAGENAMESPACE)/harbor-db
|
||||
DOCKERIMAGENAME_CHART_SERVER=$(IMAGENAMESPACE)/chartmuseum-photon
|
||||
DOCKERIMAGENAME_REGCTL=$(IMAGENAMESPACE)/harbor-registryctl
|
||||
DOCKERIMAGENAME_EXPORTER=$(IMAGENAMESPACE)/harbor-exporter
|
||||
|
||||
@ -295,10 +280,6 @@ endif
|
||||
ifeq ($(TRIVYFLAG), true)
|
||||
DOCKERSAVE_PARA+= $(IMAGENAMESPACE)/trivy-adapter-photon:$(VERSIONTAG)
|
||||
endif
|
||||
# append chartmuseum parameters if set
|
||||
ifeq ($(CHARTFLAG), true)
|
||||
DOCKERSAVE_PARA+= $(DOCKERIMAGENAME_CHART_SERVER):$(VERSIONTAG)
|
||||
endif
|
||||
|
||||
|
||||
RUNCONTAINER=$(DOCKERCMD) run --rm -u $(shell id -u):$(shell id -g) -v $(BUILDPATH):$(BUILDPATH) -w $(BUILDPATH)
|
||||
@ -430,9 +411,8 @@ build:
|
||||
-e TRIVYVERSION=$(TRIVYVERSION) -e TRIVYADAPTERVERSION=$(TRIVYADAPTERVERSION) \
|
||||
-e VERSIONTAG=$(VERSIONTAG) \
|
||||
-e BUILDBIN=$(BUILDBIN) \
|
||||
-e CHARTMUSEUMVERSION=$(CHARTMUSEUMVERSION) -e CHARTMUSEUM_SRC_TAG=$(CHARTMUSEUM_SRC_TAG) -e DOCKERIMAGENAME_CHART_SERVER=$(DOCKERIMAGENAME_CHART_SERVER) \
|
||||
-e NPM_REGISTRY=$(NPM_REGISTRY) -e BASEIMAGETAG=$(BASEIMAGETAG) -e IMAGENAMESPACE=$(IMAGENAMESPACE) -e BASEIMAGENAMESPACE=$(BASEIMAGENAMESPACE) \
|
||||
-e CHARTURL=$(CHARTURL) -e NOTARYURL=$(NOTARYURL) -e REGISTRYURL=$(REGISTRYURL) \
|
||||
-e NOTARYURL=$(NOTARYURL) -e REGISTRYURL=$(REGISTRYURL) \
|
||||
-e TRIVY_DOWNLOAD_URL=$(TRIVY_DOWNLOAD_URL) -e TRIVY_ADAPTER_DOWNLOAD_URL=$(TRIVY_ADAPTER_DOWNLOAD_URL) \
|
||||
-e PULL_BASE_FROM_DOCKERHUB=$(PULL_BASE_FROM_DOCKERHUB) -e BUILD_BASE=$(BUILD_BASE) \
|
||||
-e REGISTRYUSER=$(REGISTRYUSER) -e REGISTRYPASSWORD=$(REGISTRYPASSWORD) \
|
||||
|
@ -7646,11 +7646,6 @@ definitions:
|
||||
x-nullable: true
|
||||
x-omitempty: true
|
||||
description: If the Harbor instance is deployed with nested notary.
|
||||
with_chartmuseum:
|
||||
type: boolean
|
||||
x-nullable: true
|
||||
x-omitempty: true
|
||||
description: If the Harbor instance is deployed with nested chartmuseum.
|
||||
registry_url:
|
||||
type: string
|
||||
x-nullable: true
|
||||
|
@ -56,7 +56,7 @@ data_volume: /data
|
||||
# Uncomment storage_service setting If you want to using external storage
|
||||
# storage_service:
|
||||
# # ca_bundle is the path to the custom root ca certificate, which will be injected into the truststore
|
||||
# # of registry's and chart repository's containers. This is usually needed when the user hosts a internal storage with self signed certificate.
|
||||
# # of registry's containers. This is usually needed when the user hosts a internal storage with self signed certificate.
|
||||
# ca_bundle:
|
||||
|
||||
# # storage backend, default is filesystem, options include filesystem, azure, gcs, s3, swift and oss
|
||||
@ -117,10 +117,6 @@ notification:
|
||||
# Maximum retry count for webhook job
|
||||
webhook_job_max_retry: 10
|
||||
|
||||
chart:
|
||||
# Change the value of absolute_url to enabled can enable absolute url in chart
|
||||
absolute_url: disabled
|
||||
|
||||
# Log configurations
|
||||
log:
|
||||
# options are debug, info, warning, error, fatal
|
||||
@ -187,7 +183,6 @@ _version: 2.7.0
|
||||
# # db_index 0 is for core, it's unchangeable
|
||||
# registry_db_index: 1
|
||||
# jobservice_db_index: 2
|
||||
# chartmuseum_db_index: 3
|
||||
# trivy_db_index: 5
|
||||
# idle_timeout_seconds: 30
|
||||
|
||||
|
@ -9,8 +9,7 @@ set +o noglob
|
||||
|
||||
usage=$'Please set hostname and other necessary attributes in harbor.yml first. DO NOT use localhost or 127.0.0.1 for hostname, because Harbor needs to be accessed by external clients.
|
||||
Please set --with-notary if needs enable Notary in Harbor, and set ui_url_protocol/ssl_cert/ssl_cert_key in harbor.yml bacause notary must run under https.
|
||||
Please set --with-trivy if needs enable Trivy in Harbor
|
||||
Please set --with-chartmuseum if needs enable Chartmuseum in Harbor'
|
||||
Please set --with-trivy if needs enable Trivy in Harbor'
|
||||
item=0
|
||||
|
||||
# notary is not enabled by default
|
||||
@ -19,8 +18,6 @@ with_notary=$false
|
||||
with_clair=$false
|
||||
# trivy is not enabled by default
|
||||
with_trivy=$false
|
||||
# chartmuseum is not enabled by default
|
||||
with_chartmuseum=$false
|
||||
|
||||
# flag to using docker compose v1 or v2, default would using v1 docker-compose
|
||||
DOCKER_COMPOSE=docker-compose
|
||||
@ -36,8 +33,6 @@ while [ $# -gt 0 ]; do
|
||||
with_clair=true;;
|
||||
--with-trivy)
|
||||
with_trivy=true;;
|
||||
--with-chartmuseum)
|
||||
with_chartmuseum=true;;
|
||||
*)
|
||||
note "$usage"
|
||||
exit 1;;
|
||||
@ -83,10 +78,6 @@ if [ $with_trivy ]
|
||||
then
|
||||
prepare_para="${prepare_para} --with-trivy"
|
||||
fi
|
||||
if [ $with_chartmuseum ]
|
||||
then
|
||||
prepare_para="${prepare_para} --with-chartmuseum"
|
||||
fi
|
||||
|
||||
./prepare $prepare_para
|
||||
echo ""
|
||||
@ -99,12 +90,6 @@ fi
|
||||
echo ""
|
||||
|
||||
h2 "[Step $item]: starting Harbor ..."
|
||||
if [ $with_chartmuseum ]
|
||||
then
|
||||
warn "
|
||||
Chartmusuem will be deprecated as of Harbor v2.6.0 and start to be removed in v2.8.0 or later.
|
||||
Please see discussion here for more details. https://github.com/goharbor/harbor/discussions/15057"
|
||||
fi
|
||||
if [ $with_notary ]
|
||||
then
|
||||
warn "
|
||||
|
@ -117,14 +117,6 @@ DOCKERFILEPATH_EXPORTER=$(DOCKERFILEPATH)/$(EXPORTER)
|
||||
DOCKERFILENAME_EXPORTER=Dockerfile
|
||||
DOCKERIMAGENAME_EXPORTER=$(IMAGENAMESPACE)/harbor-$(EXPORTER)
|
||||
|
||||
# for chart server (chartmuseum)
|
||||
CHARTSERVER=chartserver
|
||||
DOCKERFILEPATH_CHART_SERVER=$(DOCKERFILEPATH)/$(CHARTSERVER)
|
||||
DOCKERFILENAME_CHART_SERVER=Dockerfile
|
||||
CHART_SERVER_CODE_BASE=https://github.com/helm/chartmuseum.git
|
||||
CHART_SERVER_MAIN_PATH=cmd/chartmuseum
|
||||
CHART_SERVER_BIN_NAME=chartm
|
||||
|
||||
_build_prepare:
|
||||
@$(call _build_base,$(PREPARE),$(DOCKERFILEPATH_PREPARE))
|
||||
@echo "building prepare container for photon..."
|
||||
@ -184,21 +176,6 @@ _build_trivy_adapter:
|
||||
echo "Done." ; \
|
||||
fi
|
||||
|
||||
_build_chart_server:
|
||||
@if [ "$(CHARTFLAG)" = "true" ] ; then \
|
||||
$(call _build_base,$(CHARTSERVER),$(DOCKERFILEPATH_CHART_SERVER)); \
|
||||
if [ "$(BUILDBIN)" != "true" ] ; then \
|
||||
rm -rf $(DOCKERFILEPATH_CHART_SERVER)/binary && mkdir -p $(DOCKERFILEPATH_CHART_SERVER)/binary && \
|
||||
$(call _get_binary, $(CHARTURL), $(DOCKERFILEPATH_CHART_SERVER)/binary/chartm); \
|
||||
else \
|
||||
cd $(DOCKERFILEPATH_CHART_SERVER) && $(DOCKERFILEPATH_CHART_SERVER)/builder $(GOBUILDIMAGE) $(CHART_SERVER_CODE_BASE) $(CHARTMUSEUM_SRC_TAG) $(CHART_SERVER_MAIN_PATH) $(CHART_SERVER_BIN_NAME) && cd - ; \
|
||||
fi ; \
|
||||
echo "building chartmuseum container for photon..." ; \
|
||||
$(DOCKERBUILD_WITH_PULL_PARA) --build-arg harbor_base_image_version=$(BASEIMAGETAG) --build-arg harbor_base_namespace=$(BASEIMAGENAMESPACE) -f $(DOCKERFILEPATH_CHART_SERVER)/$(DOCKERFILENAME_CHART_SERVER) -t $(DOCKERIMAGENAME_CHART_SERVER):$(VERSIONTAG) . ; \
|
||||
rm -rf $(DOCKERFILEPATH_CHART_SERVER)/binary; \
|
||||
echo "Done." ; \
|
||||
fi
|
||||
|
||||
_build_nginx:
|
||||
@$(call _build_base,$(NGINX),$(DOCKERFILEPATH_NGINX))
|
||||
@echo "building nginx container for photon..."
|
||||
@ -284,7 +261,7 @@ define _build_base
|
||||
fi
|
||||
endef
|
||||
|
||||
build: _build_prepare _build_db _build_portal _build_core _build_jobservice _build_log _build_nginx _build_registry _build_registryctl _build_notary _build_trivy_adapter _build_redis _build_chart_server _compile_and_build_exporter
|
||||
build: _build_prepare _build_db _build_portal _build_core _build_jobservice _build_log _build_nginx _build_registry _build_registryctl _build_notary _build_trivy_adapter _build_redis _compile_and_build_exporter
|
||||
@if [ -n "$(REGISTRYUSER)" ] && [ -n "$(REGISTRYPASSWORD)" ] ; then \
|
||||
docker logout ; \
|
||||
fi
|
||||
|
@ -1,23 +0,0 @@
|
||||
ARG harbor_base_image_version
|
||||
ARG harbor_base_namespace
|
||||
FROM ${harbor_base_namespace}/harbor-chartserver-base:${harbor_base_image_version}
|
||||
|
||||
COPY ./make/photon/chartserver/binary/chartm /home/chart/
|
||||
COPY ./make/photon/chartserver/docker-entrypoint.sh /home/chart/
|
||||
COPY ./make/photon/common/install_cert.sh /home/chart/
|
||||
|
||||
RUN chown -R chart:chart /etc/pki/tls/certs \
|
||||
&& chown -R chart:chart /home/chart \
|
||||
&& chmod u+x /home/chart/chartm \
|
||||
&& chmod u+x /home/chart/docker-entrypoint.sh \
|
||||
&& chmod u+x /home/chart/install_cert.sh
|
||||
|
||||
USER chart
|
||||
|
||||
WORKDIR /home/chart
|
||||
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
|
||||
VOLUME ["/chart_storage"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD curl -sS http://localhost:9999/health || curl -k -sS https://localhost:9443/health || exit 1
|
@ -1,6 +0,0 @@
|
||||
FROM photon:4.0
|
||||
|
||||
RUN tdnf install -y shadow >>/dev/null\
|
||||
&& tdnf clean all \
|
||||
&& groupadd -r -g 10000 chart \
|
||||
&& useradd --no-log-init -m -g 10000 -u 10000 chart
|
@ -1,34 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set +e
|
||||
|
||||
usage(){
|
||||
echo "Usage: builder <golang image:version> <code path> <code release tag> <main.go path> <binary name>"
|
||||
echo "e.g: builder golang:1.19.4 github.com/helm/chartmuseum v0.14.0 cmd/chartmuseum chartm"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ $# != 5 ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
GOLANG_IMAGE="$1"
|
||||
GIT_PATH="$2"
|
||||
CODE_VERSION="$3"
|
||||
MAIN_GO_PATH="$4"
|
||||
BIN_NAME="$5"
|
||||
|
||||
set -e
|
||||
|
||||
cd `dirname $0`
|
||||
cur=$PWD
|
||||
|
||||
mkdir -p binary
|
||||
rm -rf binary/$BIN_NAME || true
|
||||
cp compile.sh binary/
|
||||
cp *.patch binary/
|
||||
|
||||
docker run -i --rm -v $cur/binary:/go/bin --name golang_code_builder $GOLANG_IMAGE /bin/bash /go/bin/compile.sh $GIT_PATH $CODE_VERSION $MAIN_GO_PATH $BIN_NAME
|
||||
|
||||
#Clear
|
||||
#docker rm -f golang_code_builder
|
@ -1,36 +0,0 @@
|
||||
#!/bin/bash
|
||||
set +e
|
||||
|
||||
usage(){
|
||||
echo "Usage: compile.sh <code path> <code tag> <main.go path> <binary name>"
|
||||
echo "e.g: compile.sh github.com/helm/chartmuseum v0.14.0 cmd/chartmuseum chartm"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ $# != 4 ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
GIT_PATH="$1"
|
||||
VERSION="$2"
|
||||
MAIN_GO_PATH="$3"
|
||||
BIN_NAME="$4"
|
||||
|
||||
#Get the source code
|
||||
git clone $GIT_PATH src_code
|
||||
ls
|
||||
SRC_PATH=$(pwd)/src_code
|
||||
set -e
|
||||
|
||||
#Checkout the released tag branch
|
||||
cd $SRC_PATH
|
||||
git checkout tags/$VERSION -b $VERSION
|
||||
|
||||
#Patch
|
||||
for p in $(ls /go/bin/*.patch); do
|
||||
git apply $p || exit /b 1
|
||||
done
|
||||
|
||||
#Compile
|
||||
cd $SRC_PATH/$MAIN_GO_PATH && go build -a -o $BIN_NAME
|
||||
mv $BIN_NAME /go/bin/
|
@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
|
||||
/home/chart/install_cert.sh
|
||||
|
||||
#Start the server process
|
||||
exec /home/chart/chartm
|
@ -1,77 +0,0 @@
|
||||
diff --git a/cmd/chartmuseum/main.go b/cmd/chartmuseum/main.go
|
||||
index 8fd7349..eb67ddf 100644
|
||||
--- a/cmd/chartmuseum/main.go
|
||||
+++ b/cmd/chartmuseum/main.go
|
||||
@@ -282,12 +282,23 @@ func storeFromConfig(conf *config.Config) cache.Store {
|
||||
switch cacheFlag {
|
||||
case "redis":
|
||||
store = redisCacheFromConfig(conf)
|
||||
+ case "redis_sentinel":
|
||||
+ store = redisSentinelCacheFromConfig(conf)
|
||||
default:
|
||||
crash("Unsupported cache store: ", cacheFlag)
|
||||
}
|
||||
|
||||
return store
|
||||
}
|
||||
+func redisSentinelCacheFromConfig(conf *config.Config) cache.Store {
|
||||
+ crashIfConfigMissingVars(conf, []string{"cache.redis.addr", "cache.redis.mastername"})
|
||||
+ return cache.Store(cache.NewRedisSentinelStore(
|
||||
+ conf.GetString("cache.redis.mastername"),
|
||||
+ strings.Split(conf.GetString("cache.redis.addr"), ","),
|
||||
+ conf.GetString("cache.redis.password"),
|
||||
+ conf.GetInt("cache.redis.db"),
|
||||
+ ))
|
||||
+}
|
||||
|
||||
func redisCacheFromConfig(conf *config.Config) cache.Store {
|
||||
crashIfConfigMissingVars(conf, []string{"cache.redis.addr"})
|
||||
diff --git a/pkg/cache/redis_sentinel.go b/pkg/cache/redis_sentinel.go
|
||||
new file mode 100644
|
||||
index 0000000..0c73427
|
||||
--- /dev/null
|
||||
+++ b/pkg/cache/redis_sentinel.go
|
||||
@@ -0,0 +1,18 @@
|
||||
+package cache
|
||||
+
|
||||
+import (
|
||||
+ "github.com/go-redis/redis"
|
||||
+)
|
||||
+
|
||||
+// NewRedisStore creates a new RedisStore
|
||||
+func NewRedisSentinelStore(masterName string, sentinelAddrs []string, password string, db int) *RedisStore {
|
||||
+ store := &RedisStore{}
|
||||
+ redisClientOptions := &redis.FailoverOptions{
|
||||
+ MasterName: masterName,
|
||||
+ SentinelAddrs: sentinelAddrs,
|
||||
+ Password: password,
|
||||
+ DB: db,
|
||||
+ }
|
||||
+ store.Client = redis.NewFailoverClient(redisClientOptions)
|
||||
+ return store
|
||||
+}
|
||||
diff --git a/pkg/config/vars.go b/pkg/config/vars.go
|
||||
index 638eb49..4f036c9 100644
|
||||
--- a/pkg/config/vars.go
|
||||
+++ b/pkg/config/vars.go
|
||||
@@ -246,10 +246,19 @@ var configVars = map[string]configVar{
|
||||
Default: "",
|
||||
CLIFlag: cli.StringFlag{
|
||||
Name: "cache-redis-addr",
|
||||
- Usage: "address of Redis service (host:port)",
|
||||
+ Usage: "address of Redis service (host:port), addresses of Redis+Sentinel service (host1:port1,host2:port2)",
|
||||
EnvVar: "CACHE_REDIS_ADDR",
|
||||
},
|
||||
},
|
||||
+ "cache.redis.mastername": {
|
||||
+ Type: stringType,
|
||||
+ Default: "",
|
||||
+ CLIFlag: cli.StringFlag{
|
||||
+ Name: "cache-redis-mastername",
|
||||
+ Usage: "address of Redis+Sentinel mastername",
|
||||
+ EnvVar: "CACHE_REDIS_MASTERNAME",
|
||||
+ },
|
||||
+ },
|
||||
"cache.redis.password": {
|
||||
Type: stringType,
|
||||
Default: "",
|
@ -15,7 +15,6 @@ from utils.registry_ctl import prepare_registry_ctl
|
||||
from utils.core import prepare_core
|
||||
from utils.notary import prepare_notary
|
||||
from utils.log import prepare_log_configs
|
||||
from utils.chart import prepare_chartmuseum
|
||||
from utils.docker_compose import prepare_docker_compose
|
||||
from utils.nginx import prepare_nginx, nginx_confd_dir
|
||||
from utils.redis import prepare_redis
|
||||
@ -30,11 +29,10 @@ old_private_key_pem_path, old_crt_path)
|
||||
@click.option('--conf', default=input_config_path, help="the path of Harbor configuration file")
|
||||
@click.option('--with-notary', is_flag=True, help="the Harbor instance is to be deployed with notary")
|
||||
@click.option('--with-trivy', is_flag=True, help="the Harbor instance is to be deployed with Trivy")
|
||||
@click.option('--with-chartmuseum', is_flag=True, help="the Harbor instance is to be deployed with chart repository supporting")
|
||||
def prepare(conf, with_notary, with_trivy, with_chartmuseum):
|
||||
def prepare(conf, with_notary, with_trivy):
|
||||
|
||||
delfile(config_dir)
|
||||
config_dict = parse_yaml_config(conf, with_notary=with_notary, with_trivy=with_trivy, with_chartmuseum=with_chartmuseum)
|
||||
config_dict = parse_yaml_config(conf, with_notary=with_notary, with_trivy=with_trivy)
|
||||
try:
|
||||
validate(config_dict, notary_mode=with_notary)
|
||||
except Exception as e:
|
||||
@ -45,7 +43,7 @@ def prepare(conf, with_notary, with_trivy, with_chartmuseum):
|
||||
prepare_portal(config_dict)
|
||||
prepare_log_configs(config_dict)
|
||||
prepare_nginx(config_dict)
|
||||
prepare_core(config_dict, with_notary=with_notary, with_trivy=with_trivy, with_chartmuseum=with_chartmuseum)
|
||||
prepare_core(config_dict, with_notary=with_notary, with_trivy=with_trivy)
|
||||
prepare_registry(config_dict)
|
||||
prepare_registry_ctl(config_dict)
|
||||
prepare_db(config_dict)
|
||||
@ -72,7 +70,4 @@ def prepare(conf, with_notary, with_trivy, with_chartmuseum):
|
||||
if with_trivy:
|
||||
prepare_trivy_adapter(config_dict)
|
||||
|
||||
if with_chartmuseum:
|
||||
prepare_chartmuseum(config_dict)
|
||||
|
||||
prepare_docker_compose(config_dict, with_trivy, with_notary, with_chartmuseum)
|
||||
prepare_docker_compose(config_dict, with_trivy, with_notary)
|
||||
|
@ -57,7 +57,6 @@ INTERNAL_NO_PROXY_DN = {
|
||||
'jobservice',
|
||||
'registry',
|
||||
'registryctl',
|
||||
'chartmuseum',
|
||||
'notary-server',
|
||||
'notary-signer',
|
||||
'trivy-adapter',
|
||||
|
@ -287,7 +287,6 @@ external_redis:
|
||||
# db_index 0 is for core, it's unchangeable
|
||||
registry_db_index: {{ external_redis.registry_db_index }}
|
||||
jobservice_db_index: {{ external_redis.jobservice_db_index }}
|
||||
chartmuseum_db_index: {{ external_redis.chartmuseum_db_index }}
|
||||
clair_db_index: 4
|
||||
{% else %}
|
||||
# Umcomments external_redis if using external Redis server
|
||||
@ -298,7 +297,6 @@ external_redis:
|
||||
# # db_index 0 is for core, it's unchangeable
|
||||
# registry_db_index: 1
|
||||
# jobservice_db_index: 2
|
||||
# chartmuseum_db_index: 3
|
||||
# clair_db_index: 4
|
||||
{% endif %}
|
||||
|
||||
|
@ -226,7 +226,6 @@ external_redis:
|
||||
# db_index 0 is for core, it's unchangeable
|
||||
registry_db_index: {{ external_redis.registry_db_index }}
|
||||
jobservice_db_index: {{ external_redis.jobservice_db_index }}
|
||||
chartmuseum_db_index: {{ external_redis.chartmuseum_db_index }}
|
||||
{% else %}
|
||||
# Umcomments external_redis if using external Redis server
|
||||
# external_redis:
|
||||
@ -236,7 +235,6 @@ external_redis:
|
||||
# # db_index 0 is for core, it's unchangeable
|
||||
# registry_db_index: 1
|
||||
# jobservice_db_index: 2
|
||||
# chartmuseum_db_index: 3
|
||||
{% endif %}
|
||||
|
||||
{% if uaa is defined %}
|
||||
|
@ -330,7 +330,6 @@ external_redis:
|
||||
# db_index 0 is for core, it's unchangeable
|
||||
registry_db_index: {{ external_redis.registry_db_index }}
|
||||
jobservice_db_index: {{ external_redis.jobservice_db_index }}
|
||||
chartmuseum_db_index: {{ external_redis.chartmuseum_db_index }}
|
||||
clair_db_index: {{ external_redis.clair_db_index }}
|
||||
trivy_db_index: 5
|
||||
idle_timeout_seconds: 30
|
||||
@ -348,7 +347,6 @@ external_redis:
|
||||
# # db_index 0 is for core, it's unchangeable
|
||||
# registry_db_index: 1
|
||||
# jobservice_db_index: 2
|
||||
# chartmuseum_db_index: 3
|
||||
# clair_db_index: 4
|
||||
# trivy_db_index: 5
|
||||
# idle_timeout_seconds: 30
|
||||
|
@ -380,7 +380,6 @@ external_redis:
|
||||
# db_index 0 is for core, it's unchangeable
|
||||
registry_db_index: {{ external_redis.registry_db_index }}
|
||||
jobservice_db_index: {{ external_redis.jobservice_db_index }}
|
||||
chartmuseum_db_index: {{ external_redis.chartmuseum_db_index }}
|
||||
clair_db_index: {{ external_redis.clair_db_index }}
|
||||
trivy_db_index: 5
|
||||
idle_timeout_seconds: 30
|
||||
@ -398,7 +397,6 @@ external_redis:
|
||||
# # db_index 0 is for core, it's unchangeable
|
||||
# registry_db_index: 1
|
||||
# jobservice_db_index: 2
|
||||
# chartmuseum_db_index: 3
|
||||
# clair_db_index: 4
|
||||
# trivy_db_index: 5
|
||||
# idle_timeout_seconds: 30
|
||||
|
@ -357,7 +357,6 @@ external_redis:
|
||||
# db_index 0 is for core, it's unchangeable
|
||||
registry_db_index: {{ external_redis.registry_db_index }}
|
||||
jobservice_db_index: {{ external_redis.jobservice_db_index }}
|
||||
chartmuseum_db_index: {{ external_redis.chartmuseum_db_index }}
|
||||
trivy_db_index: 5
|
||||
idle_timeout_seconds: 30
|
||||
{% else %}
|
||||
@ -374,7 +373,6 @@ external_redis:
|
||||
# # db_index 0 is for core, it's unchangeable
|
||||
# registry_db_index: 1
|
||||
# jobservice_db_index: 2
|
||||
# chartmuseum_db_index: 3
|
||||
# trivy_db_index: 5
|
||||
# idle_timeout_seconds: 30
|
||||
{% endif %}
|
||||
|
@ -357,7 +357,6 @@ external_redis:
|
||||
# db_index 0 is for core, it's unchangeable
|
||||
registry_db_index: {{ external_redis.registry_db_index }}
|
||||
jobservice_db_index: {{ external_redis.jobservice_db_index }}
|
||||
chartmuseum_db_index: {{ external_redis.chartmuseum_db_index }}
|
||||
trivy_db_index: 5
|
||||
idle_timeout_seconds: 30
|
||||
{% else %}
|
||||
@ -374,7 +373,6 @@ external_redis:
|
||||
# # db_index 0 is for core, it's unchangeable
|
||||
# registry_db_index: 1
|
||||
# jobservice_db_index: 2
|
||||
# chartmuseum_db_index: 3
|
||||
# trivy_db_index: 5
|
||||
# idle_timeout_seconds: 30
|
||||
{% endif %}
|
||||
|
@ -363,7 +363,6 @@ external_redis:
|
||||
# db_index 0 is for core, it's unchangeable
|
||||
registry_db_index: {{ external_redis.registry_db_index }}
|
||||
jobservice_db_index: {{ external_redis.jobservice_db_index }}
|
||||
chartmuseum_db_index: {{ external_redis.chartmuseum_db_index }}
|
||||
trivy_db_index: 5
|
||||
idle_timeout_seconds: 30
|
||||
{% else %}
|
||||
@ -380,7 +379,6 @@ external_redis:
|
||||
# # db_index 0 is for core, it's unchangeable
|
||||
# registry_db_index: 1
|
||||
# jobservice_db_index: 2
|
||||
# chartmuseum_db_index: 3
|
||||
# trivy_db_index: 5
|
||||
# idle_timeout_seconds: 30
|
||||
{% endif %}
|
||||
|
@ -377,7 +377,6 @@ external_redis:
|
||||
# db_index 0 is for core, it's unchangeable
|
||||
registry_db_index: {{ external_redis.registry_db_index }}
|
||||
jobservice_db_index: {{ external_redis.jobservice_db_index }}
|
||||
chartmuseum_db_index: {{ external_redis.chartmuseum_db_index }}
|
||||
trivy_db_index: 5
|
||||
idle_timeout_seconds: 30
|
||||
{% else %}
|
||||
@ -394,7 +393,6 @@ external_redis:
|
||||
# # db_index 0 is for core, it's unchangeable
|
||||
# registry_db_index: 1
|
||||
# jobservice_db_index: 2
|
||||
# chartmuseum_db_index: 3
|
||||
# trivy_db_index: 5
|
||||
# idle_timeout_seconds: 30
|
||||
{% endif %}
|
||||
|
@ -377,7 +377,6 @@ external_redis:
|
||||
# db_index 0 is for core, it's unchangeable
|
||||
registry_db_index: {{ external_redis.registry_db_index }}
|
||||
jobservice_db_index: {{ external_redis.jobservice_db_index }}
|
||||
chartmuseum_db_index: {{ external_redis.chartmuseum_db_index }}
|
||||
trivy_db_index: 5
|
||||
idle_timeout_seconds: 30
|
||||
{% else %}
|
||||
@ -394,7 +393,6 @@ external_redis:
|
||||
# # db_index 0 is for core, it's unchangeable
|
||||
# registry_db_index: 1
|
||||
# jobservice_db_index: 2
|
||||
# chartmuseum_db_index: 3
|
||||
# trivy_db_index: 5
|
||||
# idle_timeout_seconds: 30
|
||||
{% endif %}
|
||||
|
@ -384,7 +384,6 @@ external_redis:
|
||||
# db_index 0 is for core, it's unchangeable
|
||||
registry_db_index: {{ external_redis.registry_db_index }}
|
||||
jobservice_db_index: {{ external_redis.jobservice_db_index }}
|
||||
chartmuseum_db_index: {{ external_redis.chartmuseum_db_index }}
|
||||
trivy_db_index: 5
|
||||
idle_timeout_seconds: 30
|
||||
{% else %}
|
||||
@ -401,7 +400,6 @@ external_redis:
|
||||
# # db_index 0 is for core, it's unchangeable
|
||||
# registry_db_index: 1
|
||||
# jobservice_db_index: 2
|
||||
# chartmuseum_db_index: 3
|
||||
# trivy_db_index: 5
|
||||
# idle_timeout_seconds: 30
|
||||
{% endif %}
|
||||
|
@ -29,11 +29,6 @@ class InternalTLS:
|
||||
'notary_server.crt', 'notary_server.key'
|
||||
}
|
||||
|
||||
chart_museum_filename = {
|
||||
'chartmuseum.crt',
|
||||
'chartmuseum.key'
|
||||
}
|
||||
|
||||
db_certs_filename = {
|
||||
'harbor_db.crt', 'harbor_db.key'
|
||||
}
|
||||
@ -47,8 +42,6 @@ class InternalTLS:
|
||||
self.required_filenames = self.harbor_certs_filename
|
||||
if kwargs.get('with_notary'):
|
||||
self.required_filenames.update(self.notary_certs_filename)
|
||||
if kwargs.get('with_chartmuseum'):
|
||||
self.required_filenames.update(self.chart_museum_filename)
|
||||
if kwargs.get('with_trivy'):
|
||||
self.required_filenames.update(self.trivy_certs_filename)
|
||||
if not kwargs.get('external_database'):
|
||||
|
@ -115,17 +115,6 @@ echo subjectAltName = DNS.1:notary-server > extfile.cnf
|
||||
openssl x509 -req -days $DAYS -sha256 -in notary_server.csr -CA harbor_internal_ca.crt -CAkey harbor_internal_ca.key -CAcreateserial -extfile extfile.cnf -out notary_server.crt
|
||||
|
||||
|
||||
# generate chartmuseum key
|
||||
openssl req -new \
|
||||
-newkey rsa:4096 -nodes -sha256 -keyout chartmuseum.key \
|
||||
-out chartmuseum.csr \
|
||||
-subj "/C=CN/ST=Beijing/L=Beijing/O=VMware/CN=chartmuseum"
|
||||
|
||||
# sign chartmuseum csr with CA certificate and key
|
||||
echo subjectAltName = DNS.1:chartmuseum > extfile.cnf
|
||||
openssl x509 -req -days $DAYS -sha256 -in chartmuseum.csr -CA harbor_internal_ca.crt -CAkey harbor_internal_ca.key -CAcreateserial -extfile extfile.cnf -out chartmuseum.crt
|
||||
|
||||
|
||||
# generate harbor_db key
|
||||
openssl req -new \
|
||||
-newkey rsa:4096 -nodes -sha256 -keyout harbor_db.key \
|
||||
|
@ -1,59 +0,0 @@
|
||||
## Settings should be set
|
||||
{%if internal_tls.enabled %}
|
||||
PORT=9443
|
||||
TLS_CERT=/etc/harbor/ssl/chartmuseum.crt
|
||||
TLS_KEY=/etc/harbor/ssl/chartmuseum.key
|
||||
# Uncomment if need mTLS
|
||||
# TLS_CA_CERT=/etc/harbor/ssl/harbor_internal_ca.crt
|
||||
{% else %}
|
||||
PORT=9999
|
||||
{% endif %}
|
||||
|
||||
# Only support redis now. If redis is setup, then enable cache
|
||||
CACHE={{cache_store}}
|
||||
{% if cache_redis_mastername %}
|
||||
CACHE_REDIS_ADDR={{cache_redis_addr}}
|
||||
CACHE_REDIS_MASTERNAME={{cache_redis_mastername}}
|
||||
CACHE_REDIS_PASSWORD={{cache_redis_password}}
|
||||
CACHE_REDIS_DB={{cache_redis_db_index}}
|
||||
{% else %}
|
||||
CACHE_REDIS_ADDR={{cache_redis_addr}}
|
||||
CACHE_REDIS_PASSWORD={{cache_redis_password}}
|
||||
CACHE_REDIS_DB={{cache_redis_db_index}}
|
||||
{% endif %}
|
||||
|
||||
# Credential for internal communication
|
||||
BASIC_AUTH_USER=chart_controller
|
||||
BASIC_AUTH_PASS={{core_secret}}
|
||||
|
||||
# Multiple tenants
|
||||
# Must be set with 1 to support project namespace
|
||||
DEPTH=1
|
||||
|
||||
# Backend storage driver: e.g. "local", "amazon", "google" etc.
|
||||
STORAGE={{storage_driver}}
|
||||
{% if storage_driver == "amazon" %}
|
||||
AWS_SDK_LOAD_CONFIG=1
|
||||
{% endif %}
|
||||
# Storage driver settings
|
||||
{{all_storage_driver_configs}}
|
||||
## Settings with default values. Just put here for future changes
|
||||
DEBUG=false
|
||||
LOG_JSON=true
|
||||
DISABLE_METRICS=false
|
||||
DISABLE_API=false
|
||||
DISABLE_STATEFILES=false
|
||||
ALLOW_OVERWRITE=true
|
||||
{% if chart_absolute_url %}
|
||||
CHART_URL={{public_url}}/chartrepo
|
||||
{% else %}
|
||||
CHART_URL=
|
||||
{% endif %}
|
||||
AUTH_ANONYMOUS_GET=false
|
||||
CONTEXT_PATH=
|
||||
INDEX_LIMIT=0
|
||||
MAX_STORAGE_OBJECTS=0
|
||||
MAX_UPLOAD_SIZE=20971520
|
||||
CHART_POST_FORM_FIELD_NAME=chart
|
||||
PROV_POST_FORM_FIELD_NAME=prov
|
||||
STORAGE_TIMESTAMP_TOLERANCE=1s
|
@ -2,7 +2,6 @@ CONFIG_PATH=/etc/core/app.conf
|
||||
UAA_CA_ROOT=/etc/core/certificates/uaa_ca.pem
|
||||
_REDIS_URL_CORE={{redis_url_core}}
|
||||
SYNC_QUOTA=true
|
||||
CHART_CACHE_DRIVER={{chart_cache_driver}}
|
||||
_REDIS_URL_REG={{redis_url_reg}}
|
||||
|
||||
LOG_LEVEL={{log_level}}
|
||||
@ -35,9 +34,7 @@ NOTARY_URL={{notary_url}}
|
||||
REGISTRY_STORAGE_PROVIDER_NAME={{storage_provider_name}}
|
||||
READ_ONLY=false
|
||||
RELOAD_KEY={{reload_key}}
|
||||
CHART_REPOSITORY_URL={{chart_repository_url}}
|
||||
REGISTRY_CONTROLLER_URL={{registry_controller_url}}
|
||||
WITH_CHARTMUSEUM={{with_chartmuseum}}
|
||||
REGISTRY_CREDENTIAL_USERNAME={{registry_username}}
|
||||
REGISTRY_CREDENTIAL_PASSWORD={{registry_password}}
|
||||
CSRF_KEY={{csrf_key}}
|
||||
|
@ -186,11 +186,6 @@ services:
|
||||
harbor:
|
||||
{% if with_notary %}
|
||||
harbor-notary:
|
||||
{% endif %}
|
||||
{% if with_chartmuseum %}
|
||||
harbor-chartmuseum:
|
||||
aliases:
|
||||
- harbor-core
|
||||
{% endif %}
|
||||
depends_on:
|
||||
- log
|
||||
@ -291,11 +286,6 @@ services:
|
||||
- {{data_volume}}/redis:/var/lib/redis
|
||||
networks:
|
||||
harbor:
|
||||
{% if with_chartmuseum %}
|
||||
harbor-chartmuseum:
|
||||
aliases:
|
||||
- redis
|
||||
{% endif %}
|
||||
depends_on:
|
||||
- log
|
||||
logging:
|
||||
@ -477,49 +467,6 @@ services:
|
||||
env_file:
|
||||
./common/config/trivy-adapter/env
|
||||
{% endif %}
|
||||
{% if with_chartmuseum %}
|
||||
chartmuseum:
|
||||
container_name: chartmuseum
|
||||
image: goharbor/chartmuseum-photon:{{chartmuseum_version}}
|
||||
restart: always
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- CHOWN
|
||||
- DAC_OVERRIDE
|
||||
- SETGID
|
||||
- SETUID
|
||||
networks:
|
||||
- harbor-chartmuseum
|
||||
depends_on:
|
||||
- log
|
||||
volumes:
|
||||
- {{data_volume}}/chart_storage:/chart_storage:z
|
||||
- ./common/config/chartserver:/etc/chartserver:z
|
||||
- type: bind
|
||||
source: ./common/config/shared/trust-certificates
|
||||
target: /harbor_cust_cert
|
||||
{%if internal_tls.enabled %}
|
||||
- type: bind
|
||||
source: {{internal_tls.chartmuseum_crt_path}}
|
||||
target: /etc/harbor/ssl/chartmuseum.crt
|
||||
- type: bind
|
||||
source: {{internal_tls.chartmuseum_key_path}}
|
||||
target: /etc/harbor/ssl/chartmuseum.key
|
||||
{% endif %}
|
||||
{% if gcs_keyfile %}
|
||||
- type: bind
|
||||
source: {{gcs_keyfile}}
|
||||
target: /etc/chartserver/gcs.key
|
||||
{% endif %}
|
||||
logging:
|
||||
driver: "syslog"
|
||||
options:
|
||||
syslog-address: "tcp://localhost:1514"
|
||||
tag: "chartmuseum"
|
||||
env_file:
|
||||
./common/config/chartserver/env
|
||||
{% endif %}
|
||||
{% if metric.enabled %}
|
||||
exporter:
|
||||
image: goharbor/harbor-exporter:{{version}}
|
||||
@ -553,7 +500,4 @@ networks:
|
||||
notary-sig:
|
||||
external: false
|
||||
{% endif %}
|
||||
{% if with_chartmuseum %}
|
||||
harbor-chartmuseum:
|
||||
external: false
|
||||
{% endif %}
|
||||
|
||||
|
@ -125,28 +125,6 @@ http {
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
location /chartrepo/ {
|
||||
{% if internal_tls.enabled %}
|
||||
proxy_pass https://core/chartrepo/;
|
||||
|
||||
proxy_ssl_certificate /etc/harbor/tls/proxy.crt;
|
||||
proxy_ssl_certificate_key /etc/harbor/tls/proxy.key;
|
||||
proxy_ssl_trusted_certificate /harbor_cust_cert/harbor_internal_ca.crt;
|
||||
proxy_ssl_verify_depth 2;
|
||||
proxy_ssl_verify on;
|
||||
proxy_ssl_session_reuse on;
|
||||
{% else %}
|
||||
proxy_pass http://core/chartrepo/;
|
||||
{% endif %}
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $x_forwarded_proto;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
location /v1/ {
|
||||
return 404;
|
||||
}
|
||||
|
@ -149,30 +149,6 @@ http {
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
location /chartrepo/ {
|
||||
{% if internal_tls.enabled %}
|
||||
proxy_pass https://core/chartrepo/;
|
||||
|
||||
proxy_ssl_certificate /etc/harbor/tls/proxy.crt;
|
||||
proxy_ssl_certificate_key /etc/harbor/tls/proxy.key;
|
||||
proxy_ssl_trusted_certificate /harbor_cust_cert/harbor_internal_ca.crt;
|
||||
proxy_ssl_verify_depth 2;
|
||||
proxy_ssl_verify on;
|
||||
proxy_ssl_session_reuse on;
|
||||
{% else %}
|
||||
proxy_pass http://core/chartrepo/;
|
||||
{% endif %}
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $x_forwarded_proto;
|
||||
|
||||
proxy_cookie_path / "/; Secure";
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
location /v1/ {
|
||||
return 404;
|
||||
}
|
||||
|
@ -1,122 +0,0 @@
|
||||
import os
|
||||
from urllib.parse import urlsplit
|
||||
from g import templates_dir, config_dir, data_dir, DEFAULT_UID, DEFAULT_GID
|
||||
|
||||
from .jinja import render_jinja
|
||||
from .misc import prepare_dir
|
||||
|
||||
chart_museum_temp_dir = os.path.join(templates_dir, "chartserver")
|
||||
chart_museum_env_temp = os.path.join(chart_museum_temp_dir, "env.jinja")
|
||||
|
||||
chart_museum_config_dir = os.path.join(config_dir, "chartserver")
|
||||
chart_museum_env = os.path.join(config_dir, "chartserver", "env")
|
||||
|
||||
chart_museum_data_dir = os.path.join(data_dir, 'chart_storage')
|
||||
|
||||
|
||||
def parse_redis(redis_url_chart):
|
||||
u = urlsplit(redis_url_chart)
|
||||
if not u.scheme or u.scheme == 'redis':
|
||||
return {
|
||||
'cache_store': 'redis',
|
||||
'cache_redis_addr': u.netloc.split('@')[-1],
|
||||
'cache_redis_password': u.password or '',
|
||||
'cache_redis_db_index': u.path and int(u.path[1:]) or 0,
|
||||
}
|
||||
elif u.scheme == 'redis+sentinel':
|
||||
return {
|
||||
'cache_store': 'redis_sentinel',
|
||||
'cache_redis_mastername': u.path.split('/')[1],
|
||||
'cache_redis_addr': u.netloc.split('@')[-1],
|
||||
'cache_redis_password': u.password or '',
|
||||
'cache_redis_db_index': len(u.path.split('/')) == 3 and int(u.path.split('/')[2]) or 0,
|
||||
}
|
||||
else:
|
||||
raise Exception('bad redis url for chart:' + redis_url_chart)
|
||||
|
||||
|
||||
def prepare_chartmuseum(config_dict):
|
||||
storage_provider_name = config_dict['storage_provider_name']
|
||||
storage_provider_config_map = config_dict['storage_provider_config']
|
||||
|
||||
prepare_dir(chart_museum_data_dir, uid=DEFAULT_UID, gid=DEFAULT_GID)
|
||||
prepare_dir(chart_museum_config_dir)
|
||||
|
||||
# process redis info
|
||||
cache_redis_ops = parse_redis(config_dict['redis_url_chart'])
|
||||
|
||||
|
||||
# process storage info
|
||||
#default using local file system
|
||||
storage_driver = "local"
|
||||
# storage provider configurations
|
||||
# please be aware that, we do not check the validations of the values for the specified keys
|
||||
# convert the configs to config map
|
||||
storage_provider_config_options = []
|
||||
if storage_provider_name == 's3':
|
||||
# aws s3 storage
|
||||
storage_driver = "amazon"
|
||||
storage_provider_config_options.append("STORAGE_AMAZON_BUCKET=%s" % (storage_provider_config_map.get("bucket") or '') )
|
||||
storage_provider_config_options.append("STORAGE_AMAZON_PREFIX=%s" % (storage_provider_config_map.get("rootdirectory") or '') )
|
||||
storage_provider_config_options.append("STORAGE_AMAZON_REGION=%s" % (storage_provider_config_map.get("region") or '') )
|
||||
storage_provider_config_options.append("STORAGE_AMAZON_ENDPOINT=%s" % (storage_provider_config_map.get("regionendpoint") or '') )
|
||||
storage_provider_config_options.append("AWS_ACCESS_KEY_ID=%s" % (storage_provider_config_map.get("accesskey") or '') )
|
||||
storage_provider_config_options.append("AWS_SECRET_ACCESS_KEY=%s" % (storage_provider_config_map.get("secretkey") or '') )
|
||||
elif storage_provider_name == 'gcs':
|
||||
# google cloud storage
|
||||
storage_driver = "google"
|
||||
storage_provider_config_options.append("STORAGE_GOOGLE_BUCKET=%s" % ( storage_provider_config_map.get("bucket") or '') )
|
||||
storage_provider_config_options.append("STORAGE_GOOGLE_PREFIX=%s" % ( storage_provider_config_map.get("rootdirectory") or '') )
|
||||
|
||||
if storage_provider_config_map.get("keyfile"):
|
||||
storage_provider_config_options.append('GOOGLE_APPLICATION_CREDENTIALS=%s' % '/etc/chartserver/gcs.key')
|
||||
elif storage_provider_name == 'azure':
|
||||
# azure storage
|
||||
storage_driver = "microsoft"
|
||||
storage_provider_config_options.append("STORAGE_MICROSOFT_CONTAINER=%s" % ( storage_provider_config_map.get("container") or '') )
|
||||
storage_provider_config_options.append("AZURE_STORAGE_ACCOUNT=%s" % ( storage_provider_config_map.get("accountname") or '') )
|
||||
storage_provider_config_options.append("AZURE_STORAGE_ACCESS_KEY=%s" % ( storage_provider_config_map.get("accountkey") or '') )
|
||||
storage_provider_config_options.append("STORAGE_MICROSOFT_PREFIX=/azure/harbor/charts")
|
||||
elif storage_provider_name == 'swift':
|
||||
# open stack swift
|
||||
storage_driver = "openstack"
|
||||
storage_provider_config_options.append("STORAGE_OPENSTACK_CONTAINER=%s" % ( storage_provider_config_map.get("container") or '') )
|
||||
storage_provider_config_options.append("STORAGE_OPENSTACK_PREFIX=%s" % ( storage_provider_config_map.get("rootdirectory") or '') )
|
||||
storage_provider_config_options.append("STORAGE_OPENSTACK_REGION=%s" % ( storage_provider_config_map.get("region") or '') )
|
||||
storage_provider_config_options.append("OS_AUTH_URL=%s" % ( storage_provider_config_map.get("authurl") or '') )
|
||||
storage_provider_config_options.append("OS_USERNAME=%s" % ( storage_provider_config_map.get("username") or '') )
|
||||
storage_provider_config_options.append("OS_PASSWORD=%s" % ( storage_provider_config_map.get("password") or '') )
|
||||
storage_provider_config_options.append("OS_PROJECT_ID=%s" % ( storage_provider_config_map.get("tenantid") or '') )
|
||||
storage_provider_config_options.append("OS_PROJECT_NAME=%s" % ( storage_provider_config_map.get("tenant") or '') )
|
||||
storage_provider_config_options.append("OS_DOMAIN_ID=%s" % ( storage_provider_config_map.get("domainid") or '') )
|
||||
storage_provider_config_options.append("OS_DOMAIN_NAME=%s" % ( storage_provider_config_map.get("domain") or '') )
|
||||
elif storage_provider_name == 'oss':
|
||||
# aliyun OSS
|
||||
storage_driver = "alibaba"
|
||||
bucket = storage_provider_config_map.get("bucket") or ''
|
||||
endpoint = storage_provider_config_map.get("endpoint") or ''
|
||||
if endpoint.startswith(bucket + "."):
|
||||
endpoint = endpoint.replace(bucket + ".", "")
|
||||
storage_provider_config_options.append("STORAGE_ALIBABA_BUCKET=%s" % bucket )
|
||||
storage_provider_config_options.append("STORAGE_ALIBABA_ENDPOINT=%s" % endpoint )
|
||||
storage_provider_config_options.append("STORAGE_ALIBABA_PREFIX=%s" % ( storage_provider_config_map.get("rootdirectory") or '') )
|
||||
storage_provider_config_options.append(
|
||||
"ALIBABA_CLOUD_ACCESS_KEY_ID=%s" % (storage_provider_config_map.get("accesskeyid") or ''))
|
||||
storage_provider_config_options.append(
|
||||
"ALIBABA_CLOUD_ACCESS_KEY_SECRET=%s" % (storage_provider_config_map.get("accesskeysecret") or ''))
|
||||
else:
|
||||
# use local file system
|
||||
storage_provider_config_options.append("STORAGE_LOCAL_ROOTDIR=/chart_storage")
|
||||
|
||||
# generate storage provider configuration
|
||||
all_storage_provider_configs = ('\n').join(storage_provider_config_options)
|
||||
|
||||
render_jinja(
|
||||
chart_museum_env_temp,
|
||||
chart_museum_env,
|
||||
storage_driver=storage_driver,
|
||||
all_storage_driver_configs=all_storage_provider_configs,
|
||||
public_url=config_dict['public_url'],
|
||||
chart_absolute_url=config_dict['chart_absolute_url'],
|
||||
internal_tls=config_dict['internal_tls'],
|
||||
**cache_redis_ops)
|
@ -97,7 +97,7 @@ def parse_versions():
|
||||
return versions
|
||||
|
||||
|
||||
def parse_yaml_config(config_file_path, with_notary, with_trivy, with_chartmuseum):
|
||||
def parse_yaml_config(config_file_path, with_notary, with_trivy):
|
||||
'''
|
||||
:param configs: config_parser object
|
||||
:returns: dict of configs
|
||||
@ -116,7 +116,6 @@ def parse_yaml_config(config_file_path, with_notary, with_trivy, with_chartmuseu
|
||||
'jobservice_url': 'http://jobservice:8080',
|
||||
'trivy_adapter_url': 'http://trivy-adapter:8080',
|
||||
'notary_url': 'http://notary-server:4443',
|
||||
'chart_repository_url': 'http://chartmuseum:9999'
|
||||
}
|
||||
|
||||
config_dict['hostname'] = configs["hostname"]
|
||||
@ -236,13 +235,6 @@ def parse_yaml_config(config_file_path, with_notary, with_trivy, with_chartmuseu
|
||||
config_dict['trivy_insecure'] = trivy_configs.get("insecure") or False
|
||||
config_dict['trivy_timeout'] = trivy_configs.get("timeout") or '5m0s'
|
||||
|
||||
# Chart configs
|
||||
chart_configs = configs.get("chart") or {}
|
||||
if chart_configs.get('absolute_url') == 'enabled':
|
||||
config_dict['chart_absolute_url'] = True
|
||||
else:
|
||||
config_dict['chart_absolute_url'] = False
|
||||
|
||||
# jobservice config
|
||||
js_config = configs.get('jobservice') or {}
|
||||
config_dict['max_job_workers'] = js_config["max_job_workers"]
|
||||
@ -333,7 +325,6 @@ def parse_yaml_config(config_file_path, with_notary, with_trivy, with_chartmuseu
|
||||
configs['data_volume'],
|
||||
with_notary=with_notary,
|
||||
with_trivy=with_trivy,
|
||||
with_chartmuseum=with_chartmuseum,
|
||||
external_database=config_dict['external_database'])
|
||||
else:
|
||||
config_dict['internal_tls'] = InternalTLS()
|
||||
@ -359,7 +350,6 @@ def parse_yaml_config(config_file_path, with_notary, with_trivy, with_chartmuseu
|
||||
config_dict['jobservice_url'] = 'https://jobservice:8443'
|
||||
config_dict['trivy_adapter_url'] = 'https://trivy-adapter:8443'
|
||||
# config_dict['notary_url'] = 'http://notary-server:4443'
|
||||
config_dict['chart_repository_url'] = 'https://chartmuseum:9443'
|
||||
|
||||
# purge upload configs
|
||||
purge_upload_config = configs.get('upload_purging')
|
||||
@ -450,7 +440,6 @@ def get_redis_configs(external_redis=None, with_trivy=True):
|
||||
'password': '',
|
||||
'registry_db_index': 1,
|
||||
'jobservice_db_index': 2,
|
||||
'chartmuseum_db_index': 3,
|
||||
'trivy_db_index': 5,
|
||||
'idle_timeout_seconds': 30,
|
||||
}
|
||||
@ -459,7 +448,6 @@ def get_redis_configs(external_redis=None, with_trivy=True):
|
||||
redis.update({key: value for (key, value) in external_redis.items() if value})
|
||||
|
||||
configs['redis_url_core'] = get_redis_url(0, redis)
|
||||
configs['redis_url_chart'] = get_redis_url(redis['chartmuseum_db_index'], redis)
|
||||
configs['redis_url_js'] = get_redis_url(redis['jobservice_db_index'], redis)
|
||||
configs['redis_url_reg'] = get_redis_url(redis['registry_db_index'], redis)
|
||||
|
||||
|
@ -13,24 +13,16 @@ core_conf = os.path.join(config_dir, "core", "app.conf")
|
||||
ca_download_dir = os.path.join(data_dir, 'ca_download')
|
||||
|
||||
|
||||
def prepare_core(config_dict, with_notary, with_trivy, with_chartmuseum):
|
||||
def prepare_core(config_dict, with_notary, with_trivy):
|
||||
prepare_dir(ca_download_dir, uid=DEFAULT_UID, gid=DEFAULT_GID)
|
||||
prepare_dir(core_config_dir)
|
||||
# Render Core
|
||||
# set cache for chart repo server
|
||||
# default set 'memory' mode, if redis is configured then set to 'redis'
|
||||
if len(config_dict['redis_url_core']) > 0:
|
||||
chart_cache_driver = "redis"
|
||||
else:
|
||||
chart_cache_driver = "memory"
|
||||
|
||||
render_jinja(
|
||||
core_env_template_path,
|
||||
core_conf_env,
|
||||
chart_cache_driver=chart_cache_driver,
|
||||
with_notary=with_notary,
|
||||
with_trivy=with_trivy,
|
||||
with_chartmuseum=with_chartmuseum,
|
||||
csrf_key=generate_random_string(32),
|
||||
**config_dict)
|
||||
|
||||
|
@ -8,7 +8,7 @@ docker_compose_template_path = os.path.join(templates_dir, 'docker_compose', 'do
|
||||
docker_compose_yml_path = '/compose_location/docker-compose.yml'
|
||||
|
||||
# render docker-compose
|
||||
def prepare_docker_compose(configs, with_trivy, with_notary, with_chartmuseum):
|
||||
def prepare_docker_compose(configs, with_trivy, with_notary):
|
||||
versions = parse_versions()
|
||||
VERSION_TAG = versions.get('VERSION_TAG') or 'dev'
|
||||
|
||||
@ -18,7 +18,6 @@ def prepare_docker_compose(configs, with_trivy, with_notary, with_chartmuseum):
|
||||
'redis_version': VERSION_TAG,
|
||||
'notary_version': VERSION_TAG,
|
||||
'trivy_adapter_version': VERSION_TAG,
|
||||
'chartmuseum_version': VERSION_TAG,
|
||||
'data_volume': configs['data_volume'],
|
||||
'log_location': configs['log_location'],
|
||||
'protocol': configs['protocol'],
|
||||
@ -27,7 +26,6 @@ def prepare_docker_compose(configs, with_trivy, with_notary, with_chartmuseum):
|
||||
'external_database': configs['external_database'],
|
||||
'with_notary': with_notary,
|
||||
'with_trivy': with_trivy,
|
||||
'with_chartmuseum': with_chartmuseum
|
||||
}
|
||||
|
||||
# if configs.get('registry_custom_ca_bundle_path'):
|
||||
|
@ -1,29 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
|
||||
"github.com/goharbor/harbor/src/testing"
|
||||
)
|
||||
|
||||
// createMockObjects create mock objects for chart repo related testing.
|
||||
func createMockObjects() (*httptest.Server, *Controller, error) {
|
||||
s := httptest.NewServer(testing.MockChartRepoHandler)
|
||||
backendURL, err := url.Parse(s.URL)
|
||||
if err != nil {
|
||||
s.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
mockController, err := NewController(backendURL)
|
||||
if err != nil {
|
||||
s.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return s, mockController, nil
|
||||
}
|
||||
|
||||
// Http client
|
||||
var httpClient = NewChartClient(nil)
|
@ -1,229 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
beego_cache "github.com/beego/beego/v2/client/cache"
|
||||
// Enable redis cache adaptor
|
||||
_ "github.com/beego/beego/v2/client/cache/redis"
|
||||
|
||||
hlog "github.com/goharbor/harbor/src/lib/log"
|
||||
)
|
||||
|
||||
const (
|
||||
standardExpireTime = 3600 * time.Second
|
||||
redisENVKey = "_REDIS_URL_CORE"
|
||||
cacheDriverENVKey = "CHART_CACHE_DRIVER" // "memory" or "redis"
|
||||
cacheDriverMem = "memory"
|
||||
cacheDriverRedis = "redis"
|
||||
cacheDriverRedisSentinel = "redis_sentinel"
|
||||
cacheCollectionName = "helm_chart_cache"
|
||||
maxTry = 10
|
||||
)
|
||||
|
||||
// ChartCache is designed to cache some processed data for repeated accessing
|
||||
// to improve the performance
|
||||
type ChartCache struct {
|
||||
// Cache driver
|
||||
cache beego_cache.Cache
|
||||
|
||||
// Keep the driver type
|
||||
driverType string
|
||||
|
||||
// To indicate if the chart cache is enabled
|
||||
isEnabled bool
|
||||
}
|
||||
|
||||
// ChartCacheConfig keeps the configurations of ChartCache
|
||||
type ChartCacheConfig struct {
|
||||
// Only support 'in-memory' and 'redis' now
|
||||
DriverType string
|
||||
|
||||
// Align with config
|
||||
Config string
|
||||
}
|
||||
|
||||
// NewChartCache is constructor of ChartCache
|
||||
// If return nil, that means no cache is enabled for chart repository server
|
||||
func NewChartCache(config *ChartCacheConfig) *ChartCache {
|
||||
// Never return nil object
|
||||
chartCache := &ChartCache{
|
||||
isEnabled: false,
|
||||
}
|
||||
|
||||
// Double check the configurations are what we want
|
||||
if config == nil {
|
||||
return chartCache
|
||||
}
|
||||
|
||||
if config.DriverType != cacheDriverMem && config.DriverType != cacheDriverRedis && config.DriverType != cacheDriverRedisSentinel {
|
||||
return chartCache
|
||||
}
|
||||
|
||||
if config.DriverType == cacheDriverRedis || config.DriverType == cacheDriverRedisSentinel {
|
||||
if len(config.Config) == 0 {
|
||||
return chartCache
|
||||
}
|
||||
}
|
||||
|
||||
// Try to create the upstream cache
|
||||
cache := initCacheDriver(config)
|
||||
if cache == nil {
|
||||
return chartCache
|
||||
}
|
||||
|
||||
// Cache enabled
|
||||
chartCache.isEnabled = true
|
||||
chartCache.driverType = config.DriverType
|
||||
chartCache.cache = cache
|
||||
|
||||
return chartCache
|
||||
}
|
||||
|
||||
// IsEnabled to indicate if the chart cache is successfully enabled
|
||||
// The cache may be disabled if
|
||||
//
|
||||
// user does not set
|
||||
// wrong configurations
|
||||
func (chc *ChartCache) IsEnabled() bool {
|
||||
return chc.isEnabled
|
||||
}
|
||||
|
||||
// PutChart caches the detailed data of chart version
|
||||
func (chc *ChartCache) PutChart(chart *ChartVersionDetails) {
|
||||
ctx := context.Background()
|
||||
// If cache is not enabled, do nothing
|
||||
if !chc.IsEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
// As it's a valid json data anymore when retrieving back from redis cache,
|
||||
// here we use separate methods to handle the data according to the driver type
|
||||
if chart != nil {
|
||||
var err error
|
||||
|
||||
switch chc.driverType {
|
||||
case cacheDriverMem:
|
||||
// Directly put object in
|
||||
err = chc.cache.Put(ctx, chart.Metadata.Digest, chart, standardExpireTime)
|
||||
case cacheDriverRedis, cacheDriverRedisSentinel:
|
||||
// Marshal to json data before saving
|
||||
var jsonData []byte
|
||||
if jsonData, err = json.Marshal(chart); err == nil {
|
||||
err = chc.cache.Put(ctx, chart.Metadata.Digest, jsonData, standardExpireTime)
|
||||
}
|
||||
default:
|
||||
// Should not reach here, but still put guard code here
|
||||
err = errors.New("meet invalid cache driver")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Just logged
|
||||
hlog.Errorf("Failed to cache chart object with error: %s\n", err)
|
||||
hlog.Warningf("If cache driver is using 'redis', please check the related configurations or the network connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetChart trys to retrieve it from the cache
|
||||
// If hit, return the cached item;
|
||||
// otherwise, nil object is returned
|
||||
func (chc *ChartCache) GetChart(chartDigest string) *ChartVersionDetails {
|
||||
// If cache is not enabled, do nothing
|
||||
ctx := context.Background()
|
||||
if !chc.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
object, err := chc.cache.Get(ctx, chartDigest)
|
||||
if err != nil {
|
||||
hlog.Warningf("Failed to get cache value by key with error: %s", err)
|
||||
}
|
||||
if object != nil {
|
||||
// Try to convert data
|
||||
// First try the normal way
|
||||
if chartDetails, ok := object.(*ChartVersionDetails); ok {
|
||||
return chartDetails
|
||||
}
|
||||
|
||||
// Maybe json bytes
|
||||
if bytes, yes := object.([]byte); yes {
|
||||
chartDetails := &ChartVersionDetails{}
|
||||
err := json.Unmarshal(bytes, chartDetails)
|
||||
if err == nil {
|
||||
return chartDetails
|
||||
}
|
||||
// Just logged the error
|
||||
hlog.Errorf("Failed to retrieve chart from cache with error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize the cache driver based on the config
|
||||
func initCacheDriver(cacheConfig *ChartCacheConfig) beego_cache.Cache {
|
||||
switch cacheConfig.DriverType {
|
||||
case cacheDriverMem:
|
||||
hlog.Info("Enable memory cache for chart caching")
|
||||
return beego_cache.NewMemoryCache()
|
||||
case cacheDriverRedis:
|
||||
// New with retry
|
||||
count := 0
|
||||
for {
|
||||
count++
|
||||
redisCache, err := beego_cache.NewCache(cacheDriverRedis, cacheConfig.Config)
|
||||
if err != nil {
|
||||
// Just logged
|
||||
hlog.Errorf("Failed to initialize redis cache: %s", err)
|
||||
|
||||
if count < maxTry {
|
||||
<-time.After(time.Duration(backoff(count)) * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
hlog.Info("Enable redis cache for chart caching")
|
||||
return redisCache
|
||||
}
|
||||
case cacheDriverRedisSentinel:
|
||||
// New with retry
|
||||
count := 0
|
||||
for {
|
||||
count++
|
||||
redisCache, err := beego_cache.NewCache(cacheDriverRedisSentinel, cacheConfig.Config)
|
||||
if err != nil {
|
||||
// Just logged
|
||||
hlog.Errorf("Failed to initialize redis sentinel cache: %s", err)
|
||||
|
||||
if count < maxTry {
|
||||
<-time.After(time.Duration(backoff(count)) * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
hlog.Info("Enable redis sentinel cache for chart caching")
|
||||
return redisCache
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
// Any other cases
|
||||
hlog.Info("No cache is enabled for chart caching")
|
||||
return nil
|
||||
}
|
||||
|
||||
// backoff: fast->slow->fast
|
||||
func backoff(count int) int {
|
||||
f := 5 - math.Abs((float64)(count)-5)
|
||||
return (int)(math.Pow(2, f))
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"helm.sh/helm/v3/pkg/chart"
|
||||
helm_repo "helm.sh/helm/v3/pkg/repo"
|
||||
)
|
||||
|
||||
var (
|
||||
mockChart = &ChartVersionDetails{
|
||||
Metadata: &helm_repo.ChartVersion{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "mock_chart",
|
||||
Version: "0.1.0",
|
||||
},
|
||||
Digest: "mock_digest",
|
||||
},
|
||||
Dependencies: make([]*chart.Dependency, 0),
|
||||
}
|
||||
)
|
||||
|
||||
// Test the no cache set scenario
|
||||
func TestNoCache(t *testing.T) {
|
||||
chartCache := NewChartCache(nil)
|
||||
if chartCache == nil {
|
||||
t.Fatalf("cache instance should not be nil")
|
||||
}
|
||||
|
||||
if chartCache.IsEnabled() {
|
||||
t.Fatal("chart cache should not be enabled")
|
||||
}
|
||||
}
|
||||
|
||||
// Test the in memory cache
|
||||
func TestInMemoryCache(t *testing.T) {
|
||||
chartCache := NewChartCache(&ChartCacheConfig{
|
||||
DriverType: cacheDriverMem,
|
||||
})
|
||||
if chartCache == nil {
|
||||
t.Fatalf("cache instance should not be nil")
|
||||
}
|
||||
|
||||
if !chartCache.IsEnabled() {
|
||||
t.Fatal("chart cache should be enabled")
|
||||
}
|
||||
|
||||
if chartCache.driverType != cacheDriverMem {
|
||||
t.Fatalf("expect driver type %s but got %s", cacheDriverMem, chartCache.driverType)
|
||||
}
|
||||
|
||||
chartCache.PutChart(mockChart)
|
||||
theCachedChart := chartCache.GetChart(mockChart.Metadata.Digest)
|
||||
if theCachedChart == nil || theCachedChart.Metadata.Name != mockChart.Metadata.Name {
|
||||
t.Fatal("In memory cache does work")
|
||||
}
|
||||
}
|
||||
|
||||
// Test redis cache
|
||||
// Failed to config redis cache and then use in memory instead
|
||||
func TestRedisCache(t *testing.T) {
|
||||
redisConfigV := make(map[string]string)
|
||||
redisConfigV["key"] = cacheCollectionName
|
||||
redisConfigV["conn"] = ":6379"
|
||||
redisConfigV["dbNum"] = "0"
|
||||
redisConfigV["password"] = ""
|
||||
|
||||
redisConfig, _ := json.Marshal(redisConfigV)
|
||||
|
||||
chartCache := NewChartCache(&ChartCacheConfig{
|
||||
DriverType: cacheDriverRedis,
|
||||
Config: string(redisConfig),
|
||||
})
|
||||
if chartCache == nil {
|
||||
t.Fatalf("cache instance should not be nil")
|
||||
}
|
||||
|
||||
if !chartCache.IsEnabled() {
|
||||
t.Fatal("chart cache should be enabled")
|
||||
}
|
||||
|
||||
if chartCache.driverType != cacheDriverRedis {
|
||||
t.Fatalf("expect driver type '%s' but got '%s'", cacheDriverRedis, chartCache.driverType)
|
||||
}
|
||||
|
||||
chartCache.PutChart(mockChart)
|
||||
theCachedChart := chartCache.GetChart(mockChart.Metadata.Digest)
|
||||
if theCachedChart == nil || theCachedChart.Metadata.Name != mockChart.Metadata.Name {
|
||||
t.Fatal("In memory cache does work")
|
||||
}
|
||||
}
|
@ -1,262 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver"
|
||||
"helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/chart/loader"
|
||||
"helm.sh/helm/v3/pkg/chartutil"
|
||||
helm_repo "helm.sh/helm/v3/pkg/repo"
|
||||
|
||||
hlog "github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/pkg/label/model"
|
||||
)
|
||||
|
||||
const (
|
||||
readmeFileName = "README.md"
|
||||
valuesFileName = "values.yaml"
|
||||
)
|
||||
|
||||
// ChartVersion extends the helm ChartVersion with additional labels
|
||||
type ChartVersion struct {
|
||||
helm_repo.ChartVersion
|
||||
Labels []*model.Label `json:"labels"`
|
||||
}
|
||||
|
||||
// ChartVersions is an array of extended ChartVersion
|
||||
type ChartVersions []*ChartVersion
|
||||
|
||||
// ChartVersionDetails keeps the detailed data info of the chart version
|
||||
type ChartVersionDetails struct {
|
||||
Metadata *helm_repo.ChartVersion `json:"metadata"`
|
||||
Dependencies []*chart.Dependency `json:"dependencies"`
|
||||
Values map[string]interface{} `json:"values"`
|
||||
Files map[string]string `json:"files"`
|
||||
Security *SecurityReport `json:"security"`
|
||||
Labels []*model.Label `json:"labels"`
|
||||
}
|
||||
|
||||
// SecurityReport keeps the info related with security
|
||||
// e.g.: digital signature, vulnerability scanning etc.
|
||||
type SecurityReport struct {
|
||||
Signature *DigitalSignature `json:"signature"`
|
||||
}
|
||||
|
||||
// DigitalSignature used to indicate if the chart has been signed
|
||||
type DigitalSignature struct {
|
||||
Signed bool `json:"signed"`
|
||||
Provenance string `json:"prov_file"`
|
||||
}
|
||||
|
||||
// ChartInfo keeps the information of the chart
|
||||
type ChartInfo struct {
|
||||
Name string `json:"name"`
|
||||
TotalVersions uint32 `json:"total_versions"`
|
||||
LatestVersion string `json:"latest_version"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
Icon string `json:"icon"`
|
||||
Home string `json:"home"`
|
||||
Deprecated bool `json:"deprecated"`
|
||||
}
|
||||
|
||||
// ChartOperator is designed to process the contents of
|
||||
// the specified chart version to get more details
|
||||
type ChartOperator struct{}
|
||||
|
||||
// GetChartDetails parse the details from the provided content bytes
|
||||
func (cho *ChartOperator) GetChartDetails(content []byte) (*ChartVersionDetails, error) {
|
||||
chartData, err := cho.GetChartData(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dependencies := chartData.Metadata.Dependencies
|
||||
var values map[string]interface{}
|
||||
var buf bytes.Buffer
|
||||
files := make(map[string]string)
|
||||
// Parse values
|
||||
if chartData.Values != nil {
|
||||
// values = parseRawValues([]byte(chartData.Values.GetRaw()))
|
||||
if len(chartData.Values) > 0 {
|
||||
c := chartutil.Values(chartData.Values)
|
||||
ValYaml, err := c.YAML()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = c.Encode(&buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values = parseRawValues(buf.Bytes())
|
||||
// Append values.yaml file
|
||||
files[valuesFileName] = ValYaml
|
||||
}
|
||||
}
|
||||
|
||||
// Append other files like 'README.md'
|
||||
for _, v := range chartData.Files {
|
||||
if v.Name == readmeFileName {
|
||||
files[readmeFileName] = string(v.Data)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
theChart := &ChartVersionDetails{
|
||||
Dependencies: dependencies,
|
||||
Values: values,
|
||||
Files: files,
|
||||
}
|
||||
|
||||
return theChart, nil
|
||||
}
|
||||
|
||||
// GetChartList returns a reorganized chart list
|
||||
func (cho *ChartOperator) GetChartList(content []byte) ([]*ChartInfo, error) {
|
||||
if len(content) == 0 {
|
||||
return nil, errors.New("zero content")
|
||||
}
|
||||
|
||||
allCharts := make(map[string]helm_repo.ChartVersions)
|
||||
if err := json.Unmarshal(content, &allCharts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chartList := make([]*ChartInfo, 0)
|
||||
for key, chartVersions := range allCharts {
|
||||
lVersion, oVersion := getTheTwoCharts(chartVersions)
|
||||
if lVersion != nil && oVersion != nil {
|
||||
chartInfo := &ChartInfo{
|
||||
Name: key,
|
||||
TotalVersions: uint32(len(chartVersions)),
|
||||
}
|
||||
chartInfo.Created = oVersion.Created
|
||||
chartInfo.Home = lVersion.Home
|
||||
chartInfo.Icon = lVersion.Icon
|
||||
chartInfo.Deprecated = lVersion.Deprecated
|
||||
chartInfo.LatestVersion = lVersion.Version
|
||||
chartList = append(chartList, chartInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the chart list by the updated time which is the create time
|
||||
// of the latest version of the chart.
|
||||
sort.Slice(chartList, func(i, j int) bool {
|
||||
if chartList[i].Updated.Equal(chartList[j].Updated) {
|
||||
return strings.Compare(chartList[i].Name, chartList[j].Name) < 0
|
||||
}
|
||||
|
||||
return chartList[i].Updated.After(chartList[j].Updated)
|
||||
})
|
||||
|
||||
return chartList, nil
|
||||
}
|
||||
|
||||
// GetChartData returns raw data of chart
|
||||
func (cho *ChartOperator) GetChartData(content []byte) (*chart.Chart, error) {
|
||||
if len(content) == 0 {
|
||||
return nil, errors.New("zero content")
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(content)
|
||||
chartData, err := loader.LoadArchive(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return chartData, nil
|
||||
}
|
||||
|
||||
// GetChartVersions returns the chart versions
|
||||
func (cho *ChartOperator) GetChartVersions(content []byte) (ChartVersions, error) {
|
||||
if len(content) == 0 {
|
||||
return nil, errors.New("zero content")
|
||||
}
|
||||
|
||||
chartVersions := make(ChartVersions, 0)
|
||||
if err := json.Unmarshal(content, &chartVersions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return chartVersions, nil
|
||||
}
|
||||
|
||||
// Get the latest and oldest chart versions
|
||||
func getTheTwoCharts(chartVersions helm_repo.ChartVersions) (latestChart *helm_repo.ChartVersion, oldestChart *helm_repo.ChartVersion) {
|
||||
if len(chartVersions) == 1 {
|
||||
return chartVersions[0], chartVersions[0]
|
||||
}
|
||||
|
||||
for _, chartVersion := range chartVersions {
|
||||
currentV, err := semver.NewVersion(chartVersion.Version)
|
||||
if err != nil {
|
||||
// ignore it, just logged
|
||||
hlog.Warningf("Malformed semversion %s for the chart %s", chartVersion.Version, chartVersion.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// Find latest chart
|
||||
if latestChart == nil {
|
||||
latestChart = chartVersion
|
||||
} else {
|
||||
lVersion, err := semver.NewVersion(latestChart.Version)
|
||||
if err != nil {
|
||||
// ignore it, just logged
|
||||
hlog.Warningf("Malformed semversion %s for the chart %s", latestChart.Version, chartVersion.Name)
|
||||
continue
|
||||
}
|
||||
if lVersion.LessThan(currentV) {
|
||||
latestChart = chartVersion
|
||||
}
|
||||
}
|
||||
|
||||
if oldestChart == nil {
|
||||
oldestChart = chartVersion
|
||||
} else {
|
||||
if oldestChart.Created.After(chartVersion.Created) {
|
||||
oldestChart = chartVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return latestChart, oldestChart
|
||||
}
|
||||
|
||||
// Parse the raw values to value map
|
||||
func parseRawValues(rawValue []byte) map[string]interface{} {
|
||||
valueMap := make(map[string]interface{})
|
||||
|
||||
if len(rawValue) == 0 {
|
||||
return valueMap
|
||||
}
|
||||
values, err := chartutil.ReadValues(rawValue)
|
||||
if err != nil || len(values) == 0 {
|
||||
return valueMap
|
||||
}
|
||||
|
||||
readValue(values, "", valueMap)
|
||||
|
||||
return valueMap
|
||||
}
|
||||
|
||||
// Recursively read value
|
||||
func readValue(values map[string]interface{}, keyPrefix string, valueMap map[string]interface{}) {
|
||||
for key, value := range values {
|
||||
longKey := key
|
||||
if keyPrefix != "" {
|
||||
longKey = fmt.Sprintf("%s.%s", keyPrefix, key)
|
||||
}
|
||||
|
||||
if subValues, ok := value.(map[string]interface{}); ok {
|
||||
readValue(subValues, longKey, valueMap)
|
||||
} else {
|
||||
valueMap[longKey] = value
|
||||
}
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
)
|
||||
|
||||
func TestGetChartDetails(t *testing.T) {
|
||||
chartOpr := ChartOperator{}
|
||||
chartDetails, err := chartOpr.GetChartDetails(htesting.HelmChartContent)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(chartDetails.Dependencies) == 0 {
|
||||
t.Fatal("At least 1 dependency exitsing, but we got 0 now")
|
||||
}
|
||||
|
||||
if len(chartDetails.Values) == 0 {
|
||||
t.Fatal("At least 1 value existing, but we got 0 now")
|
||||
}
|
||||
|
||||
if chartDetails.Values["adminserver.adminPassword"] != "Harbor12345" {
|
||||
t.Fatalf("The value of 'adminserver.adminPassword' should be 'Harbor12345' but we got '%s' now", chartDetails.Values["adminserver.adminPassword"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetChartList(t *testing.T) {
|
||||
chartOpr := ChartOperator{}
|
||||
infos, err := chartOpr.GetChartList(htesting.ChartListContent)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(infos) != 2 {
|
||||
t.Fatalf("Length of chart list should be 2, but we got %d now", len(infos))
|
||||
}
|
||||
|
||||
firstInSortedList := infos[0]
|
||||
if firstInSortedList.Name != "harbor" {
|
||||
t.Fatalf("Expect the fist item of the sorted list to be 'harbor' but got '%s'", firstInSortedList.Name)
|
||||
}
|
||||
|
||||
if firstInSortedList.LatestVersion != "0.2.0" {
|
||||
t.Fatalf("Expect latest version '0.2.0' but got '%s'", firstInSortedList.LatestVersion)
|
||||
}
|
||||
}
|
@ -1,148 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
|
||||
commonhttp "github.com/goharbor/harbor/src/common/http"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
tracelib "github.com/goharbor/harbor/src/lib/trace"
|
||||
)
|
||||
|
||||
const (
|
||||
clientTimeout = 30 * time.Second
|
||||
maxIdleConnections = 10
|
||||
idleConnectionTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
chartTransport http.RoundTripper
|
||||
)
|
||||
|
||||
// ChartClient is a http client to get the content from the external http server
|
||||
type ChartClient struct {
|
||||
// HTTP client
|
||||
httpClient *http.Client
|
||||
|
||||
// Auth info
|
||||
credential *Credential
|
||||
}
|
||||
|
||||
// NewChartClient is constructor of ChartClient
|
||||
// credential can be nil
|
||||
func NewChartClient(credential *Credential) *ChartClient { // Create http client with customized timeouts
|
||||
once.Do(func() {
|
||||
chartTransport = commonhttp.NewTransport(
|
||||
commonhttp.WithMaxIdleConns(maxIdleConnections),
|
||||
commonhttp.WithIdleconnectionTimeout(idleConnectionTimeout),
|
||||
)
|
||||
if tracelib.Enabled() {
|
||||
chartTransport = otelhttp.NewTransport(chartTransport)
|
||||
}
|
||||
})
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: clientTimeout,
|
||||
Transport: chartTransport,
|
||||
}
|
||||
|
||||
return &ChartClient{
|
||||
httpClient: client,
|
||||
credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// GetContent get the bytes from the specified url
|
||||
func (cc *ChartClient) GetContent(addr string) ([]byte, error) {
|
||||
response, err := cc.sendRequest(addr, http.MethodGet, nil)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "get content failed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "Read response body error")
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
text, err := extractError(content)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "Extract content error failed")
|
||||
return nil, err
|
||||
}
|
||||
return nil, &commonhttp.Error{
|
||||
Code: response.StatusCode,
|
||||
Message: text,
|
||||
}
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// DeleteContent sends deleting request to the addr to delete content
|
||||
func (cc *ChartClient) DeleteContent(addr string) error {
|
||||
response, err := cc.sendRequest(addr, http.MethodDelete, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
text, err := extractError(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return &commonhttp.Error{
|
||||
Code: response.StatusCode,
|
||||
Message: text,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendRequest sends requests to the addr with the specified spec
|
||||
func (cc *ChartClient) sendRequest(addr string, method string, body io.Reader) (*http.Response, error) {
|
||||
if len(strings.TrimSpace(addr)) == 0 {
|
||||
return nil, errors.New("empty url is not allowed")
|
||||
}
|
||||
|
||||
fullURI, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "Invalid url")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(method, addr, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set basic auth
|
||||
if cc.credential != nil {
|
||||
request.SetBasicAuth(cc.credential.Username, cc.credential.Password)
|
||||
}
|
||||
|
||||
response, err := cc.httpClient.Do(request)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("send request %s %s failed", method, fullURI.Path))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
hlog "github.com/goharbor/harbor/src/lib/log"
|
||||
)
|
||||
|
||||
const (
|
||||
userName = "chart_controller"
|
||||
passwordKey = "CORE_SECRET"
|
||||
)
|
||||
|
||||
// Credential keeps the username and password for the basic auth
|
||||
type Credential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// Controller is used to handle flows of related requests based on the corresponding handlers
|
||||
// A reverse proxy will be created and managed to proxy the related traffics between API and
|
||||
// backend chart server
|
||||
type Controller struct {
|
||||
// Proxy used to to transfer the traffic of requests
|
||||
// It's mainly used to talk to the backend chart server
|
||||
trafficProxy *ProxyEngine
|
||||
|
||||
// Parse and process the chart version to provide required info data
|
||||
chartOperator *ChartOperator
|
||||
|
||||
// HTTP client used to call the realted APIs of the backend chart repositories
|
||||
apiClient *ChartClient
|
||||
|
||||
// The access endpoint of the backend chart repository server
|
||||
backendServerAddress *url.URL
|
||||
|
||||
// Cache the chart data
|
||||
chartCache *ChartCache
|
||||
}
|
||||
|
||||
// NewController is constructor of the chartserver.Controller
|
||||
func NewController(backendServer *url.URL, middlewares ...func(http.Handler) http.Handler) (*Controller, error) {
|
||||
if backendServer == nil {
|
||||
return nil, errors.New("failed to create chartserver.Controller: backend sever address is required")
|
||||
}
|
||||
|
||||
// Try to create credential
|
||||
cred := &Credential{
|
||||
Username: userName,
|
||||
Password: os.Getenv(passwordKey),
|
||||
}
|
||||
|
||||
// Creat cache
|
||||
cacheCfg, err := getCacheConfig()
|
||||
if err != nil {
|
||||
// just log the error
|
||||
// will not break the whole flow if failed to create cache
|
||||
hlog.Errorf("failed to get cache configuration with error: %s", err)
|
||||
}
|
||||
cache := NewChartCache(cacheCfg)
|
||||
if !cache.IsEnabled() {
|
||||
hlog.Info("No cache is enabled for chart caching")
|
||||
}
|
||||
|
||||
return &Controller{
|
||||
backendServerAddress: backendServer,
|
||||
// Use customized reverse proxy
|
||||
trafficProxy: NewProxyEngine(backendServer, cred, middlewares...),
|
||||
// Initialize chart operator for use
|
||||
chartOperator: &ChartOperator{},
|
||||
// Create http client with customized timeouts
|
||||
apiClient: NewChartClient(cred),
|
||||
chartCache: cache,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// APIPrefix returns the API prefix path of calling backend chart service.
|
||||
func (c *Controller) APIPrefix(namespace string) string {
|
||||
return fmt.Sprintf("%s/api/%s/charts", c.backendServerAddress.String(), namespace)
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test controller
|
||||
func TestController(t *testing.T) {
|
||||
s, c, err := createMockObjects()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
prefix := c.APIPrefix("fake")
|
||||
expected := fmt.Sprintf("%s/api/%s/charts", s.URL, "fake")
|
||||
if prefix != expected {
|
||||
t.Fatalf("expect '%s' but got '%s'", expected, prefix)
|
||||
}
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"helm.sh/helm/v3/cmd/helm/search"
|
||||
helm_repo "helm.sh/helm/v3/pkg/repo"
|
||||
)
|
||||
|
||||
// ServiceHandler defines the related methods to handle kinds of chart service requests.
|
||||
type ServiceHandler interface {
|
||||
// ListCharts lists all the charts under the specified namespace.
|
||||
//
|
||||
// namespace string: the chart namespace.
|
||||
//
|
||||
// If succeed, a chart info list with nil error will be returned;
|
||||
// otherwise, a non-nil error will be got.
|
||||
ListCharts(namespace string) ([]*ChartInfo, error)
|
||||
|
||||
// Get all the chart versions of the specified chart under the namespace.
|
||||
//
|
||||
// namespace string: the chart namespace.
|
||||
// chartName string: the name of the chart, e.g: "harbor"
|
||||
//
|
||||
// If succeed, a chart version list with nil error will be returned;
|
||||
// otherwise, a non-nil error will be got.
|
||||
GetChart(namespace, chartName string) (helm_repo.ChartVersions, error)
|
||||
|
||||
// Get the detailed info of the specified chart version under the namespace.
|
||||
// The detailed info includes chart summary, dependencies, values and signature status etc.
|
||||
//
|
||||
// namespace string: the chart namespace.
|
||||
// chartName string: the name of the chart, e.g: "harbor"
|
||||
// version string: the SemVer version of the chart, e.g: "0.2.0"
|
||||
//
|
||||
// If succeed, chart version details with nil error will be returned;
|
||||
// otherwise, a non-nil error will be got.
|
||||
GetChartVersionDetails(namespace, chartName, version string) (*ChartVersionDetails, error)
|
||||
|
||||
// SearchChart search charts in the specified namespaces with the keyword q.
|
||||
// RegExp mode is enabled as default.
|
||||
// For each chart, only the latest version will shown in the result list if matched to avoid duplicated entries.
|
||||
// Keep consistent with `helm search` command.
|
||||
//
|
||||
// q string : the searching keyword
|
||||
// namespaces []string : the search namespace scope
|
||||
//
|
||||
// If succeed, a search result list with nil error will be returned;
|
||||
// otherwise, a non-nil error will be got.
|
||||
SearchChart(q string, namespaces []string) ([]*search.Result, error)
|
||||
|
||||
// GetIndexFile will read the index.yaml under all namespaces and merge them as a single one
|
||||
// Please be aware that, to support this function, the backend chart repository server should
|
||||
// enable multi-tenancies
|
||||
//
|
||||
// namespaces []string : all the namespaces with accessing permissions
|
||||
//
|
||||
// If succeed, a unified merged index file with nil error will be returned;
|
||||
// otherwise, a non-nil error will be got.
|
||||
GetIndexFile(namespaces []string) (*helm_repo.IndexFile, error)
|
||||
|
||||
// Get the chart summary of the specified chart version.
|
||||
//
|
||||
// namespace string: the chart namespace.
|
||||
// chartName string: the name of the chart, e.g: "harbor"
|
||||
// version string: the SemVer version of the chart, e.g: "0.2.0"
|
||||
//
|
||||
// If succeed, chart version summary with nil error will be returned;
|
||||
// otherwise, a non-nil error will be got.
|
||||
GetChartVersion(namespace, name, version string) (*helm_repo.ChartVersion, error)
|
||||
|
||||
// DeleteChart deletes all the chart versions of the specified chart under the namespace.
|
||||
//
|
||||
// namespace string: the chart namespace.
|
||||
// chartName string: the name of the chart, e.g: "harbor"
|
||||
//
|
||||
// If succeed, a nil error will be returned;
|
||||
// otherwise, a non-nil error will be got.
|
||||
DeleteChart(namespace, chartName string) error
|
||||
|
||||
// GetCountOfCharts calculates and returns the total count of charts under the specified namespaces.
|
||||
//
|
||||
// namespaces []string : the namespaces to count charts
|
||||
//
|
||||
// If succeed, a unsigned integer with nil error will be returned;
|
||||
// otherwise, a non-nil error will be got.
|
||||
GetCountOfCharts(namespaces []string) (uint64, error)
|
||||
}
|
||||
|
||||
// ProxyTrafficHandler defines the handler methods to handle the proxy traffic.
|
||||
type ProxyTrafficHandler interface {
|
||||
// Proxy the traffic to the backended server
|
||||
//
|
||||
// Req *http.Request : The incoming http request
|
||||
// w http.ResponseWriter : The response writer reference
|
||||
ProxyTraffic(w http.ResponseWriter, req *http.Request)
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
helm_repo "helm.sh/helm/v3/pkg/repo"
|
||||
|
||||
commonhttp "github.com/goharbor/harbor/src/common/http"
|
||||
rep_event "github.com/goharbor/harbor/src/controller/event/handler/replication/event"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/pkg/reg/model"
|
||||
)
|
||||
|
||||
// ListCharts gets the chart list under the namespace
|
||||
// See @ServiceHandler.ListCharts
|
||||
func (c *Controller) ListCharts(namespace string) ([]*ChartInfo, error) {
|
||||
if len(strings.TrimSpace(namespace)) == 0 {
|
||||
return nil, errors.New("empty namespace when getting chart list")
|
||||
}
|
||||
|
||||
content, err := c.apiClient.GetContent(c.APIPrefix(namespace))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.chartOperator.GetChartList(content)
|
||||
}
|
||||
|
||||
// GetChart returns all the chart versions under the specified chart
|
||||
// See @ServiceHandler.GetChart
|
||||
func (c *Controller) GetChart(namespace, chartName string) (ChartVersions, error) {
|
||||
if len(namespace) == 0 {
|
||||
return nil, errors.New("empty name when getting chart versions")
|
||||
}
|
||||
|
||||
if len(chartName) == 0 {
|
||||
return nil, errors.New("no chart name specified")
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s", c.APIPrefix(namespace), chartName)
|
||||
data, err := c.apiClient.GetContent(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
versions := make(ChartVersions, 0)
|
||||
if err := json.Unmarshal(data, &versions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
// DeleteChartVersion will delete the specified version of the chart
|
||||
// See @ServiceHandler.DeleteChartVersion
|
||||
func (c *Controller) DeleteChartVersion(namespace, chartName, version string) error {
|
||||
if len(namespace) == 0 {
|
||||
return errors.New("empty namespace when deleting chart version")
|
||||
}
|
||||
|
||||
if len(chartName) == 0 || len(version) == 0 {
|
||||
return errors.New("invalid chart for deleting")
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("/api/chartrepo/%s/charts/%s/%s", namespace, chartName, version)
|
||||
req, _ := http.NewRequest(http.MethodDelete, url, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
c.trafficProxy.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
text, err := extractError(w.Body.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return &commonhttp.Error{
|
||||
Code: w.Code,
|
||||
Message: text,
|
||||
}
|
||||
}
|
||||
|
||||
// send notification to replication handler
|
||||
// Todo: it used as the replacement of webhook, will be removed when webhook to be introduced.
|
||||
if os.Getenv("UTTEST") != "true" {
|
||||
go func() {
|
||||
e := &rep_event.Event{
|
||||
Type: rep_event.EventTypeChartDelete,
|
||||
Resource: &model.Resource{
|
||||
Type: model.ResourceTypeChart,
|
||||
Deleted: true,
|
||||
Metadata: &model.ResourceMetadata{
|
||||
Repository: &model.Repository{
|
||||
Name: fmt.Sprintf("%s/%s", namespace, chartName),
|
||||
},
|
||||
Artifacts: []*model.Artifact{
|
||||
{
|
||||
Tags: []string{version},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := rep_event.Handle(orm.Context(), e); err != nil {
|
||||
log.Errorf("failed to handle event: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetChartVersion returns the summary of the specified chart version.
|
||||
// See @ServiceHandler.GetChartVersion
|
||||
func (c *Controller) GetChartVersion(namespace, name, version string) (*helm_repo.ChartVersion, error) {
|
||||
if len(namespace) == 0 {
|
||||
return nil, errors.New("empty namespace when getting summary of chart version")
|
||||
}
|
||||
|
||||
if len(name) == 0 || len(version) == 0 {
|
||||
return nil, errors.New("invalid chart when getting summary")
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s/%s", c.APIPrefix(namespace), name, version)
|
||||
|
||||
content, err := c.apiClient.GetContent(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chartVersion := &helm_repo.ChartVersion{}
|
||||
if err := yaml.Unmarshal(content, chartVersion); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return chartVersion, nil
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test get /api/:repo/charts/harbor
|
||||
func TestGetChart(t *testing.T) {
|
||||
s, c, err := createMockObjects()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
versions, err := c.GetChart("repo1", "harbor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(versions) != 2 {
|
||||
t.Fatalf("expect 2 chart versions of harbor but got %d", len(versions))
|
||||
}
|
||||
}
|
||||
|
||||
// Test delete /api/:repo/charts/harbor/0.2.0
|
||||
func TestDeleteChartVersion(t *testing.T) {
|
||||
s, c, err := createMockObjects()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
if err := c.DeleteChartVersion("repo1", "harbor", "0.2.0"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test get /api/:repo/charts
|
||||
func TestRetrieveChartList(t *testing.T) {
|
||||
s, c, err := createMockObjects()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
chartList, err := c.ListCharts("repo1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(chartList) != 2 {
|
||||
t.Fatalf("Expect to get 2 charts in the list but got %d", len(chartList))
|
||||
}
|
||||
|
||||
foundItem := false
|
||||
for _, chartInfo := range chartList {
|
||||
if chartInfo.Name == "hello-helm" && chartInfo.TotalVersions == 2 {
|
||||
foundItem = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundItem {
|
||||
t.Fatalf("Expect chart 'hello-helm' with 2 versions but got nothing")
|
||||
}
|
||||
}
|
||||
|
||||
// Test the GetChartVersion in utility handler
|
||||
func TestGetChartVersionSummary(t *testing.T) {
|
||||
s, c, err := createMockObjects()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
chartV, err := c.GetChartVersion("repo1", "harbor", "0.2.0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if chartV.Name != "harbor" {
|
||||
t.Fatalf("expect chart name 'harbor' but got '%s'", chartV.Name)
|
||||
}
|
||||
|
||||
if chartV.Version != "0.2.0" {
|
||||
t.Fatalf("expect chart version '0.2.0' but got '%s'", chartV.Version)
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ProxyTraffic implements the interface method.
|
||||
func (c *Controller) ProxyTraffic(w http.ResponseWriter, req *http.Request) {
|
||||
if c.trafficProxy != nil {
|
||||
c.trafficProxy.ServeHTTP(w, req)
|
||||
}
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
helm_repo "helm.sh/helm/v3/pkg/repo"
|
||||
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
)
|
||||
|
||||
// The frontend server
|
||||
var frontServer = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mockController.ProxyTraffic(w, r)
|
||||
}))
|
||||
|
||||
var mockServer *httptest.Server
|
||||
var mockController *Controller
|
||||
|
||||
// Prepare case
|
||||
func TestStartMockServers(t *testing.T) {
|
||||
s, c, err := createMockObjects()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mockController = c
|
||||
mockServer = s
|
||||
|
||||
frontServer.Start()
|
||||
}
|
||||
|
||||
// Test /health
|
||||
func TestGetHealthOfBaseHandler(t *testing.T) {
|
||||
content, err := httpClient.GetContent(fmt.Sprintf("%s/api/chartrepo/health", frontServer.URL))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
status := make(map[string]interface{})
|
||||
if err := json.Unmarshal(content, &status); err != nil {
|
||||
t.Fatalf("Unmarshal error: %s, %s", err, content)
|
||||
}
|
||||
healthy, ok := status["health"].(bool)
|
||||
if !ok || !healthy {
|
||||
t.Fatalf("Expect healthy of server to be 'true' but got %v", status["health"])
|
||||
}
|
||||
}
|
||||
|
||||
// Get /repo1/index.yaml
|
||||
func TestGetIndexYamlByRepo(t *testing.T) {
|
||||
indexFile, err := getIndexYaml("/chartrepo/repo1/index.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(indexFile.Entries) != 3 {
|
||||
t.Fatalf("Expect index file with 3 entries, but got %d", len(indexFile.Entries))
|
||||
}
|
||||
}
|
||||
|
||||
// Test download /:repo/charts/chart.tar
|
||||
// Use this case to test the proxy function
|
||||
func TestDownloadChart(t *testing.T) {
|
||||
content, err := httpClient.GetContent(fmt.Sprintf("%s/chartrepo/repo1/charts/harbor-0.2.0.tgz", frontServer.URL))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gotSize := len(content)
|
||||
expectSize := len(htesting.HelmChartContent)
|
||||
|
||||
if gotSize != expectSize {
|
||||
t.Fatalf("Expect %d bytes data but got %d bytes", expectSize, gotSize)
|
||||
}
|
||||
}
|
||||
|
||||
// Get /api/repo1/charts/harbor
|
||||
// 401 will be rewritten to 500 with specified error
|
||||
func TestResponseRewrite(t *testing.T) {
|
||||
response, err := http.Get(fmt.Sprintf("%s/chartrepo/repo3/charts/harbor-0.8.1.tgz", frontServer.URL))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusInternalServerError {
|
||||
t.Fatalf("Expect status code 500 but got %d", response.StatusCode)
|
||||
}
|
||||
|
||||
bytes, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Read bytes from http response failed with error: %s", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
errObj := make(map[string]interface{})
|
||||
if err = json.Unmarshal(bytes, &errObj); err != nil {
|
||||
t.Fatalf("Unmarshal error: %s", err)
|
||||
}
|
||||
|
||||
if msg, ok := errObj["error"]; !ok {
|
||||
t.Fatal("Expect an error message from server but got nothing")
|
||||
} else {
|
||||
if !strings.Contains(msg.(string), "operation request from unauthorized source is rejected") {
|
||||
t.Fatal("Missing the required error message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear env
|
||||
func TestStopMockServers(t *testing.T) {
|
||||
frontServer.Close()
|
||||
mockServer.Close()
|
||||
}
|
||||
|
||||
// Utility method for getting index yaml file
|
||||
func getIndexYaml(path string) (*helm_repo.IndexFile, error) {
|
||||
content, err := httpClient.GetContent(fmt.Sprintf("%s%s", frontServer.URL, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
indexFile := &helm_repo.IndexFile{}
|
||||
if err := yaml.Unmarshal(content, indexFile); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal error: %s", err)
|
||||
}
|
||||
|
||||
if indexFile == nil {
|
||||
return nil, fmt.Errorf("got nil index yaml file")
|
||||
}
|
||||
|
||||
return indexFile, nil
|
||||
}
|
@ -1,219 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
helm_repo "helm.sh/helm/v3/pkg/repo"
|
||||
|
||||
hlog "github.com/goharbor/harbor/src/lib/log"
|
||||
)
|
||||
|
||||
const (
|
||||
maxWorkers = 10
|
||||
|
||||
// Keep consistent with 'helm search' command
|
||||
searchMaxScore = 25
|
||||
)
|
||||
|
||||
// Result returned by worker
|
||||
type processedResult struct {
|
||||
namespace string
|
||||
indexFileOfRepo *helm_repo.IndexFile
|
||||
}
|
||||
|
||||
// GetIndexFile will read the index.yaml under all namespaces and merge them as a single one
|
||||
// Please be aware that, to support this function, the backend chart repository server should
|
||||
// enable multi-tenancies
|
||||
//
|
||||
// See @ServiceHandler.GetIndexFile
|
||||
func (c *Controller) GetIndexFile(namespaces []string) (*helm_repo.IndexFile, error) {
|
||||
if len(namespaces) == 0 {
|
||||
return emptyIndexFile(), nil
|
||||
}
|
||||
|
||||
return c.getIndexYaml(namespaces)
|
||||
}
|
||||
|
||||
// getIndexYaml will get the index yaml files for all the namespaces and merge them
|
||||
// as one unified index yaml file.
|
||||
func (c *Controller) getIndexYaml(namespaces []string) (*helm_repo.IndexFile, error) {
|
||||
// The final merged index file
|
||||
mergedIndexFile := &helm_repo.IndexFile{
|
||||
APIVersion: "v1",
|
||||
Entries: make(map[string]helm_repo.ChartVersions),
|
||||
Generated: time.Now().Round(time.Second),
|
||||
PublicKeys: []string{},
|
||||
}
|
||||
|
||||
// Sync the output results from the retriever
|
||||
resultChan := make(chan *processedResult, 1)
|
||||
// Receive error
|
||||
errorChan := make(chan error, 1)
|
||||
// Signal chan for merging work
|
||||
mergeDone := make(chan struct{}, 1)
|
||||
// Total projects/namespaces
|
||||
total := len(namespaces)
|
||||
// Initialize
|
||||
initialItemCount := maxWorkers
|
||||
if total < maxWorkers {
|
||||
initialItemCount = total
|
||||
}
|
||||
// Retrieve index.yaml for repositories
|
||||
workerPool := make(chan struct{}, initialItemCount)
|
||||
|
||||
// Add initial tokens to the worker
|
||||
for i := 0; i < initialItemCount; i++ {
|
||||
workerPool <- struct{}{}
|
||||
}
|
||||
// Track all the background threads
|
||||
waitGroup := new(sync.WaitGroup)
|
||||
|
||||
// Start the index files merging thread
|
||||
go func() {
|
||||
defer func() {
|
||||
mergeDone <- struct{}{}
|
||||
}()
|
||||
|
||||
for res := range resultChan {
|
||||
c.mergeIndexFile(res.namespace, mergedIndexFile, res.indexFileOfRepo)
|
||||
}
|
||||
}()
|
||||
|
||||
// Retrieve the index files for the repositories
|
||||
// and blocking here
|
||||
var err error
|
||||
LOOP:
|
||||
for _, ns := range namespaces {
|
||||
// Check if error has occurred in some goroutines
|
||||
select {
|
||||
case err = <-errorChan:
|
||||
break LOOP
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// Apply one token before processing
|
||||
<-workerPool
|
||||
|
||||
waitGroup.Add(1)
|
||||
go func(ns string) {
|
||||
defer func() {
|
||||
waitGroup.Done() // done
|
||||
// Return the worker back to the worker
|
||||
workerPool <- struct{}{}
|
||||
}()
|
||||
|
||||
indexFile, err := c.getIndexYamlWithNS(ns)
|
||||
if err != nil {
|
||||
if len(errorChan) == 0 {
|
||||
// Only need one error as failure signal
|
||||
errorChan <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Output
|
||||
resultChan <- &processedResult{
|
||||
namespace: ns,
|
||||
indexFileOfRepo: indexFile,
|
||||
}
|
||||
}(ns)
|
||||
}
|
||||
|
||||
// Hold util all the retrieving work are done
|
||||
waitGroup.Wait()
|
||||
|
||||
// close merge channel
|
||||
close(resultChan)
|
||||
|
||||
// Wait until merging thread quit
|
||||
<-mergeDone
|
||||
|
||||
// All the threads are done
|
||||
// Make sure error in the chan is read
|
||||
if err == nil && len(errorChan) > 0 {
|
||||
err = <-errorChan
|
||||
}
|
||||
|
||||
// Met an error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Remove duplicated keys in public key list
|
||||
hash := make(map[string]string)
|
||||
for _, key := range mergedIndexFile.PublicKeys {
|
||||
hash[key] = key
|
||||
}
|
||||
mergedIndexFile.PublicKeys = []string{}
|
||||
for k := range hash {
|
||||
mergedIndexFile.PublicKeys = append(mergedIndexFile.PublicKeys, k)
|
||||
}
|
||||
|
||||
return mergedIndexFile, nil
|
||||
}
|
||||
|
||||
// Get the index yaml file under the specified namespace from the backend server
|
||||
func (c *Controller) getIndexYamlWithNS(namespace string) (*helm_repo.IndexFile, error) {
|
||||
// Join url path
|
||||
url := path.Join(namespace, "index.yaml")
|
||||
url = fmt.Sprintf("%s/%s", c.backendServerAddress.String(), url)
|
||||
hlog.Debugf("Getting index.yaml from '%s'", url)
|
||||
|
||||
content, err := c.apiClient.GetContent(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Traverse to index file object for merging
|
||||
indexFile := helm_repo.NewIndexFile()
|
||||
if err := yaml.Unmarshal(content, indexFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return indexFile, nil
|
||||
}
|
||||
|
||||
// Merge the content of mergingIndexFile to the baseIndex
|
||||
// The chart url should be without --chart-url prefix
|
||||
func (c *Controller) mergeIndexFile(namespace string,
|
||||
baseIndex *helm_repo.IndexFile,
|
||||
mergingIndexFile *helm_repo.IndexFile) {
|
||||
// Append entries
|
||||
for chartName, chartVersions := range mergingIndexFile.Entries {
|
||||
nameWithNS := fmt.Sprintf("%s/%s", namespace, chartName)
|
||||
for _, version := range chartVersions {
|
||||
version.Name = nameWithNS
|
||||
// Currently there is only one url
|
||||
for index, url := range version.URLs {
|
||||
if !strings.HasPrefix(url, "http") {
|
||||
version.URLs[index] = path.Join(namespace, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Appended
|
||||
baseIndex.Entries[nameWithNS] = chartVersions
|
||||
}
|
||||
|
||||
// Update generated time
|
||||
if mergingIndexFile.Generated.After(baseIndex.Generated) {
|
||||
baseIndex.Generated = mergingIndexFile.Generated
|
||||
}
|
||||
|
||||
// Merge public keys
|
||||
baseIndex.PublicKeys = append(baseIndex.PublicKeys, mergingIndexFile.PublicKeys...)
|
||||
}
|
||||
|
||||
// Generate empty index file
|
||||
func emptyIndexFile() *helm_repo.IndexFile {
|
||||
emptyIndexFile := &helm_repo.IndexFile{}
|
||||
emptyIndexFile.Generated = time.Now().Round(time.Second)
|
||||
|
||||
return emptyIndexFile
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import "testing"
|
||||
|
||||
// Test get /index.yaml
|
||||
func TestGetIndexFile(t *testing.T) {
|
||||
s, c, err := createMockObjects()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
namespaces := []string{"repo1", "repo2"}
|
||||
indexFile, err := c.GetIndexFile(namespaces)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(indexFile.Entries) != 5 {
|
||||
t.Fatalf("Expect index file with 5 entries, but got %d", len(indexFile.Entries))
|
||||
}
|
||||
|
||||
_, ok := indexFile.Entries["repo1/harbor"]
|
||||
if !ok {
|
||||
t.Fatal("Expect chart entry 'repo1/harbor' but got nothing")
|
||||
}
|
||||
|
||||
_, ok = indexFile.Entries["repo2/harbor"]
|
||||
if !ok {
|
||||
t.Fatal("Expect chart entry 'repo2/harbor' but got nothing")
|
||||
}
|
||||
}
|
@ -1,233 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"helm.sh/helm/v3/cmd/helm/search"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
hlog "github.com/goharbor/harbor/src/lib/log"
|
||||
)
|
||||
|
||||
const (
|
||||
maxDeletionThreads = 10
|
||||
)
|
||||
|
||||
// GetCountOfCharts calculates and returns the total count of charts under the specified namespaces.
|
||||
// See @ServiceHandler.GetCountOfCharts
|
||||
func (c *Controller) GetCountOfCharts(namespaces []string) (uint64, error) {
|
||||
if len(namespaces) == 0 {
|
||||
return 0, nil // Directly return 0 instead of non-nil error
|
||||
}
|
||||
|
||||
indexFile, err := c.getIndexYaml(namespaces)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return (uint64)(len(indexFile.Entries)), nil
|
||||
}
|
||||
|
||||
// DeleteChart deletes all the chart versions of the specified chart under the namespace.
|
||||
// See @ServiceHandler.DeleteChart
|
||||
func (c *Controller) DeleteChart(namespace, chartName string) error {
|
||||
if len(strings.TrimSpace(namespace)) == 0 {
|
||||
return errors.New("empty namespace when deleting chart")
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(chartName)) == 0 {
|
||||
return errors.New("empty chart name when deleting chart")
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s", c.APIPrefix(namespace), chartName)
|
||||
|
||||
content, err := c.apiClient.GetContent(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allVersions, err := c.chartOperator.GetChartVersions(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Let's delete the versions in parallel
|
||||
// The number of goroutine is controlled by the const maxDeletionThreads
|
||||
qSize := len(allVersions)
|
||||
if qSize > maxDeletionThreads {
|
||||
qSize = maxDeletionThreads
|
||||
}
|
||||
tokenQueue := make(chan struct{}, qSize)
|
||||
errChan := make(chan error, 1)
|
||||
waitGroup := new(sync.WaitGroup)
|
||||
waitGroup.Add(len(allVersions))
|
||||
|
||||
// Append initial tokens
|
||||
for i := 0; i < qSize; i++ {
|
||||
tokenQueue <- struct{}{}
|
||||
}
|
||||
|
||||
// Collect errors
|
||||
errs := make([]error, 0)
|
||||
errWrapper := make(chan error, 1)
|
||||
go func() {
|
||||
defer func() {
|
||||
// pass to the out func
|
||||
if len(errs) > 0 {
|
||||
errWrapper <- fmt.Errorf("%v", errs)
|
||||
}
|
||||
close(errWrapper)
|
||||
}()
|
||||
|
||||
for deletionErr := range errChan {
|
||||
errs = append(errs, deletionErr)
|
||||
}
|
||||
}()
|
||||
|
||||
// Schedule deletion tasks
|
||||
for _, deletingVersion := range allVersions {
|
||||
// Apply for token first
|
||||
// If no available token, pending here
|
||||
<-tokenQueue
|
||||
|
||||
// Got one token
|
||||
go func(deletingVersion *ChartVersion) {
|
||||
defer func() {
|
||||
// return the token back
|
||||
tokenQueue <- struct{}{}
|
||||
|
||||
// done
|
||||
waitGroup.Done()
|
||||
}()
|
||||
|
||||
if err := c.DeleteChartVersion(namespace, chartName, deletingVersion.Version); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
}(deletingVersion)
|
||||
}
|
||||
|
||||
// Wait all goroutines are done
|
||||
waitGroup.Wait()
|
||||
// Safe to quit error collection goroutine
|
||||
close(errChan)
|
||||
|
||||
err = <-errWrapper
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetChartVersionDetails get the specified version for one chart
|
||||
// This handler should return the details of the chart version,
|
||||
// maybe including metadata,dependencies and values etc.
|
||||
// See @ServiceHandler.GetChartVersionDetails
|
||||
func (c *Controller) GetChartVersionDetails(namespace, chartName, version string) (*ChartVersionDetails, error) {
|
||||
chartV, err := c.GetChartVersion(namespace, chartName, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Query cache
|
||||
chartDetails := c.chartCache.GetChart(chartV.Digest)
|
||||
if chartDetails == nil {
|
||||
// NOT hit!!
|
||||
content, err := c.getChartVersionContent(namespace, chartV.URLs[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Process bytes and get more details of chart version
|
||||
chartDetails, err = c.chartOperator.GetChartDetails(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chartDetails.Metadata = chartV
|
||||
|
||||
// Put it into the cache for next access
|
||||
c.chartCache.PutChart(chartDetails)
|
||||
} else {
|
||||
// Just logged
|
||||
hlog.Debugf("Get detailed data from cache for chart: %s:%s (%s)",
|
||||
chartDetails.Metadata.Name,
|
||||
chartDetails.Metadata.Version,
|
||||
chartDetails.Metadata.Digest)
|
||||
}
|
||||
// The change of prov file will not cause any influence to the digest of chart,
|
||||
// and then the digital signature status should be not cached
|
||||
//
|
||||
// Generate the security report
|
||||
// prov file share same endpoint with the chart version
|
||||
// Just add .prov suffix to the chart version to form the path of prov file
|
||||
// Anyway, there will be a report about the digital signature status
|
||||
chartDetails.Security = &SecurityReport{
|
||||
Signature: &DigitalSignature{
|
||||
Signed: false,
|
||||
},
|
||||
}
|
||||
// Try to get the prov file to confirm if it is exitsing
|
||||
provFilePath := fmt.Sprintf("%s.prov", chartV.URLs[0])
|
||||
provBytes, err := c.getChartVersionContent(namespace, provFilePath)
|
||||
if err == nil && len(provBytes) > 0 {
|
||||
chartDetails.Security.Signature.Signed = true
|
||||
chartDetails.Security.Signature.Provenance = provFilePath
|
||||
} else {
|
||||
// Just log it
|
||||
hlog.Debugf("Failed to get prov file for chart %s with error: %s, got %d bytes", chartV.Name, err.Error(), len(provBytes))
|
||||
}
|
||||
|
||||
return chartDetails, nil
|
||||
}
|
||||
|
||||
// SearchChart search charts in the specified namespaces with the keyword q.
|
||||
// RegExp mode is enabled as default.
|
||||
// For each chart, only the latest version will shown in the result list if matched to avoid duplicated entries.
|
||||
// Keep consistent with `helm search` command.
|
||||
func (c *Controller) SearchChart(q string, namespaces []string) ([]*search.Result, error) {
|
||||
if len(q) == 0 || len(namespaces) == 0 {
|
||||
// Return empty list
|
||||
return []*search.Result{}, nil
|
||||
}
|
||||
|
||||
// Get the merged index yaml file of the namespaces
|
||||
ind, err := c.getIndexYaml(namespaces)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build the search index
|
||||
index := search.NewIndex()
|
||||
// As the repo name is already merged into the index yaml, we use empty repo name.
|
||||
// Set 'All' to false to return only one version for each chart.
|
||||
index.AddRepo("", ind, false)
|
||||
|
||||
// Search
|
||||
// RegExp is enabled
|
||||
results, err := index.Search(q, searchMaxScore, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sort results.
|
||||
search.SortScore(results)
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Get the content bytes of the chart version
|
||||
func (c *Controller) getChartVersionContent(namespace string, subPath string) ([]byte, error) {
|
||||
var url string
|
||||
if strings.HasPrefix(subPath, "http") {
|
||||
extEndpoint, err := config.ExtEndpoint()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can not get ext endpoint")
|
||||
}
|
||||
url = strings.TrimPrefix(subPath, fmt.Sprintf("%s/%s", extEndpoint, "chartrepo/"))
|
||||
} else {
|
||||
url = path.Join(namespace, subPath)
|
||||
}
|
||||
url = fmt.Sprintf("%s/%s", c.backendServerAddress.String(), url)
|
||||
return c.apiClient.GetContent(url)
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test the function GetCountOfCharts
|
||||
func TestGetCountOfCharts(t *testing.T) {
|
||||
s, c, err := createMockObjects()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
count, err := c.GetCountOfCharts([]string{})
|
||||
if err != nil {
|
||||
t.Fatalf("expect nil error but got %s", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatalf("expect 0 but got %d", count)
|
||||
}
|
||||
|
||||
namespaces := []string{"repo1", "repo2"}
|
||||
count, err = c.GetCountOfCharts(namespaces)
|
||||
if err != nil {
|
||||
t.Fatalf("expect nil error but got %s", err)
|
||||
}
|
||||
|
||||
if count != 5 {
|
||||
t.Fatalf("expect 5 but got %d", count)
|
||||
}
|
||||
|
||||
_, err = c.GetCountOfCharts([]string{"not-existing-ns"})
|
||||
if err == nil {
|
||||
t.Fatal("expect non-nil error but got nil one")
|
||||
}
|
||||
}
|
||||
|
||||
// Test the function DeleteChart
|
||||
func TestDeleteChart(t *testing.T) {
|
||||
s, c, err := createMockObjects()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
if err := c.DeleteChart("repo1", "harbor"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test get /api/:repo/charts/:chart_name/:version
|
||||
func TestGetChartVersion(t *testing.T) {
|
||||
s, c, err := createMockObjects()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
chartVersion, err := c.GetChartVersionDetails("repo1", "harbor", "0.2.0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if chartVersion.Metadata.Name != "harbor" {
|
||||
t.Fatalf("Expect harbor chart version but got %s", chartVersion.Metadata.Name)
|
||||
}
|
||||
|
||||
if chartVersion.Metadata.Version != "0.2.0" {
|
||||
t.Fatalf("Expect version '0.2.0' but got version %s", chartVersion.Metadata.Version)
|
||||
}
|
||||
|
||||
if len(chartVersion.Dependencies) != 1 {
|
||||
t.Fatalf("Expect 1 dependency but got %d ones", len(chartVersion.Dependencies))
|
||||
}
|
||||
|
||||
if len(chartVersion.Values) != 99 {
|
||||
t.Fatalf("Expect 99 k-v values but got %d", len(chartVersion.Values))
|
||||
}
|
||||
}
|
||||
|
||||
// Test get /api/:repo/charts/:chart_name/:version with none-existing version
|
||||
func TestGetChartVersionWithError(t *testing.T) {
|
||||
s, c, err := createMockObjects()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
_, err = c.GetChartVersionDetails("repo1", "harbor", "1.0.0")
|
||||
if err == nil {
|
||||
t.Fatal("Expect an error but got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// Test the chart searching
|
||||
func TestChartSearching(t *testing.T) {
|
||||
s, c, err := createMockObjects()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
namespaces := []string{"repo1", "repo2"}
|
||||
q := "harbor"
|
||||
|
||||
results, err := c.SearchChart(q, namespaces)
|
||||
if err != nil {
|
||||
t.Fatalf("expect nil error but got '%s'", err)
|
||||
}
|
||||
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("expect 2 results but got %d", len(results))
|
||||
}
|
||||
}
|
@ -1,254 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/FZambia/sentinel"
|
||||
"github.com/beego/beego/v2/client/cache"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultKey the collection name of redis for cache adapter.
|
||||
DefaultKey = "beecacheRedis"
|
||||
)
|
||||
|
||||
// Cache is Redis cache adapter.
|
||||
type Cache struct {
|
||||
p *redis.Pool // redis connection pool
|
||||
conninfo string
|
||||
dbNum int
|
||||
key string
|
||||
password string
|
||||
maxIdle int
|
||||
masterName string
|
||||
}
|
||||
|
||||
// NewRedisCache create new redis cache with default collection name.
|
||||
func NewRedisCache() cache.Cache {
|
||||
return &Cache{key: DefaultKey}
|
||||
}
|
||||
|
||||
// actually do the redis cmds, args[0] must be the key name.
|
||||
func (rc *Cache) do(commandName string, args ...interface{}) (reply interface{}, err error) {
|
||||
if len(args) < 1 {
|
||||
return nil, errors.New("missing required arguments")
|
||||
}
|
||||
args[0] = rc.associate(args[0])
|
||||
c := rc.p.Get()
|
||||
defer c.Close()
|
||||
|
||||
return c.Do(commandName, args...)
|
||||
}
|
||||
|
||||
// associate with config key.
|
||||
func (rc *Cache) associate(originKey interface{}) string {
|
||||
return fmt.Sprintf("%s:%s", rc.key, originKey)
|
||||
}
|
||||
|
||||
// Get cache from redis.
|
||||
func (rc *Cache) Get(ctx context.Context, key string) (interface{}, error) {
|
||||
v, err := rc.do("GET", key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
// GetMulti get cache from redis.
|
||||
func (rc *Cache) GetMulti(ctx context.Context, keys []string) ([]interface{}, error) {
|
||||
c := rc.p.Get()
|
||||
defer c.Close()
|
||||
var args []interface{}
|
||||
for _, key := range keys {
|
||||
args = append(args, rc.associate(key))
|
||||
}
|
||||
values, err := redis.Values(c.Do("MGET", args...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// Put put cache to redis.
|
||||
func (rc *Cache) Put(ctx context.Context, key string, val interface{}, timeout time.Duration) error {
|
||||
_, err := rc.do("SETEX", key, int64(timeout/time.Second), val)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete delete cache in redis.
|
||||
func (rc *Cache) Delete(ctx context.Context, key string) error {
|
||||
_, err := rc.do("DEL", key)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsExist check cache's existence in redis.
|
||||
func (rc *Cache) IsExist(ctx context.Context, key string) (bool, error) {
|
||||
v, err := redis.Bool(rc.do("EXISTS", key))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Incr increase counter in redis.
|
||||
func (rc *Cache) Incr(ctx context.Context, key string) error {
|
||||
_, err := redis.Bool(rc.do("INCRBY", key, 1))
|
||||
return err
|
||||
}
|
||||
|
||||
// Decr decrease counter in redis.
|
||||
func (rc *Cache) Decr(ctx context.Context, key string) error {
|
||||
_, err := redis.Bool(rc.do("INCRBY", key, -1))
|
||||
return err
|
||||
}
|
||||
|
||||
// ClearAll clean all cache in redis. delete this redis collection.
|
||||
func (rc *Cache) ClearAll(ctx context.Context) error {
|
||||
c := rc.p.Get()
|
||||
defer c.Close()
|
||||
cachedKeys, err := redis.Strings(c.Do("KEYS", rc.key+":*"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, str := range cachedKeys {
|
||||
if _, err = c.Do("DEL", str); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// StartAndGC start redis cache adapter.
|
||||
// config is like {"key":"collection key","conn":"connection info","dbNum":"0","masterName":"mymaster"}
|
||||
// the cache item in redis are stored forever,
|
||||
// so no gc operation.
|
||||
func (rc *Cache) StartAndGC(config string) error {
|
||||
var cf map[string]string
|
||||
err := json.Unmarshal([]byte(config), &cf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := cf["key"]; !ok {
|
||||
cf["key"] = DefaultKey
|
||||
}
|
||||
if _, ok := cf["masterName"]; !ok {
|
||||
return errors.New("config has no masterName")
|
||||
}
|
||||
if _, ok := cf["conn"]; !ok {
|
||||
return errors.New("config has no conn key")
|
||||
}
|
||||
|
||||
// Format redis://<password>@<host>:<port>
|
||||
cf["conn"] = strings.Replace(cf["conn"], "redis://", "", 1)
|
||||
cf["conn"] = strings.Replace(cf["conn"], "redis_sentinel://", "", 1)
|
||||
if i := strings.Index(cf["conn"], "@"); i > -1 {
|
||||
cf["password"] = cf["conn"][0:i]
|
||||
cf["conn"] = cf["conn"][i+1:]
|
||||
}
|
||||
|
||||
if _, ok := cf["dbNum"]; !ok {
|
||||
cf["dbNum"] = "0"
|
||||
}
|
||||
if _, ok := cf["password"]; !ok {
|
||||
cf["password"] = ""
|
||||
}
|
||||
if _, ok := cf["maxIdle"]; !ok {
|
||||
cf["maxIdle"] = "3"
|
||||
}
|
||||
rc.key = cf["key"]
|
||||
rc.masterName = cf["masterName"]
|
||||
rc.conninfo = cf["conn"]
|
||||
rc.dbNum, _ = strconv.Atoi(cf["dbNum"])
|
||||
rc.password = cf["password"]
|
||||
rc.maxIdle, _ = strconv.Atoi(cf["maxIdle"])
|
||||
|
||||
rc.connectInit()
|
||||
|
||||
c := rc.p.Get()
|
||||
defer c.Close()
|
||||
|
||||
return c.Err()
|
||||
}
|
||||
|
||||
// connect to redis.
|
||||
func (rc *Cache) connectInit() {
|
||||
dialFunc := func() (c redis.Conn, err error) {
|
||||
c, err = redis.Dial("tcp", rc.conninfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rc.password != "" {
|
||||
if _, err := c.Do("AUTH", rc.password); err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
_, selecterr := c.Do("SELECT", rc.dbNum)
|
||||
if selecterr != nil {
|
||||
c.Close()
|
||||
return nil, selecterr
|
||||
}
|
||||
return
|
||||
}
|
||||
// initialize a new pool
|
||||
rc.p = &redis.Pool{
|
||||
MaxIdle: rc.maxIdle,
|
||||
IdleTimeout: 180 * time.Second,
|
||||
Dial: dialFunc,
|
||||
}
|
||||
|
||||
var sentinelOptions []redis.DialOption
|
||||
|
||||
redisOptions := sentinelOptions
|
||||
|
||||
if rc.password != "" {
|
||||
redisOptions = append(redisOptions, redis.DialPassword(rc.password))
|
||||
}
|
||||
|
||||
redisOptions = append(redisOptions, redis.DialDatabase(rc.dbNum))
|
||||
sntnl := &sentinel.Sentinel{
|
||||
Addrs: strings.Split(rc.conninfo, ","),
|
||||
MasterName: rc.masterName,
|
||||
Dial: func(addr string) (redis.Conn, error) {
|
||||
fmt.Println("chart dial redis sentinel:", addr)
|
||||
c, err := redis.Dial("tcp", addr, sentinelOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
},
|
||||
}
|
||||
|
||||
rc.p = &redis.Pool{
|
||||
Dial: func() (redis.Conn, error) {
|
||||
masterAddr, err := sntnl.MasterAddr()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println("chart dial redis master:", masterAddr, "db:", rc.dbNum)
|
||||
return redis.Dial("tcp", masterAddr, redisOptions...)
|
||||
},
|
||||
TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||
if !sentinel.TestRole(c, "master") {
|
||||
return errors.New("role check failed")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
MaxIdle: rc.maxIdle,
|
||||
IdleTimeout: 180 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
cache.Register("redis_sentinel", NewRedisCache)
|
||||
}
|
@ -1,241 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
commonhttp "github.com/goharbor/harbor/src/common/http"
|
||||
rep_event "github.com/goharbor/harbor/src/controller/event/handler/replication/event"
|
||||
"github.com/goharbor/harbor/src/controller/event/metadata"
|
||||
hlog "github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
n_event "github.com/goharbor/harbor/src/pkg/notifier/event"
|
||||
)
|
||||
|
||||
const (
|
||||
agentHarbor = "HARBOR"
|
||||
contentLengthHeader = "Content-Length"
|
||||
|
||||
defaultRepo = "library"
|
||||
rootUploadingEndpoint = "/api/chartrepo/charts"
|
||||
chartRepoHealthEndpoint = "/api/chartrepo/health"
|
||||
)
|
||||
|
||||
// ProxyEngine is used to proxy the related traffics
|
||||
type ProxyEngine struct {
|
||||
// The backend target server the traffic will be forwarded to
|
||||
// Just in case we'll use it
|
||||
backend *url.URL
|
||||
|
||||
// Use go reverse proxy as engine
|
||||
engine http.Handler
|
||||
}
|
||||
|
||||
// NewProxyEngine is constructor of NewProxyEngine
|
||||
func NewProxyEngine(target *url.URL, cred *Credential, middlewares ...func(http.Handler) http.Handler) *ProxyEngine {
|
||||
var engine http.Handler
|
||||
|
||||
engine = &httputil.ReverseProxy{
|
||||
ErrorLog: log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile),
|
||||
Director: func(req *http.Request) {
|
||||
director(target, cred, req)
|
||||
},
|
||||
ModifyResponse: modifyResponse,
|
||||
Transport: commonhttp.GetHTTPTransport(),
|
||||
}
|
||||
|
||||
if len(middlewares) > 0 {
|
||||
hlog.Info("New chart server traffic proxy with middlewares")
|
||||
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||
engine = middlewares[i](engine)
|
||||
}
|
||||
}
|
||||
|
||||
return &ProxyEngine{
|
||||
backend: target,
|
||||
engine: engine,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP serves the incoming http requests
|
||||
func (pe *ProxyEngine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
pe.engine.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
// Overwrite the http requests
|
||||
func director(target *url.URL, cred *Credential, req *http.Request) {
|
||||
// Closure
|
||||
targetQuery := target.RawQuery
|
||||
|
||||
// Overwrite the request URL to the target path
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
rewriteURLPath(req)
|
||||
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
|
||||
if targetQuery == "" || req.URL.RawQuery == "" {
|
||||
req.URL.RawQuery = targetQuery + req.URL.RawQuery
|
||||
} else {
|
||||
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
||||
}
|
||||
if _, ok := req.Header["User-Agent"]; !ok {
|
||||
req.Header.Set("User-Agent", agentHarbor)
|
||||
}
|
||||
|
||||
// Add authentication header if it is existing
|
||||
if cred != nil {
|
||||
req.SetBasicAuth(cred.Username, cred.Password)
|
||||
}
|
||||
}
|
||||
|
||||
// Modify the http response
|
||||
func modifyResponse(res *http.Response) error {
|
||||
// Upload chart success, then to the notification to replication handler
|
||||
if res.StatusCode == http.StatusCreated {
|
||||
// 201 and has chart_upload_event context
|
||||
// means this response is for uploading chart success.
|
||||
chartUploadEvent := res.Request.Context().Value(common.ChartUploadCtxKey)
|
||||
e, ok := chartUploadEvent.(*rep_event.Event)
|
||||
if !ok {
|
||||
hlog.Error("failed to convert chart upload context into replication event.")
|
||||
} else {
|
||||
// Todo: it used as the replacement of webhook, will be removed when webhook to be introduced.
|
||||
go func() {
|
||||
if err := rep_event.Handle(orm.Context(), e); err != nil {
|
||||
hlog.Errorf("failed to handle event: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Trigger harbor webhook
|
||||
if e != nil && e.Resource != nil && e.Resource.Metadata != nil && len(e.Resource.Metadata.Artifacts) > 0 &&
|
||||
len(e.Resource.ExtendedInfo) > 0 {
|
||||
event := &n_event.Event{}
|
||||
metaData := &metadata.ChartUploadMetaData{
|
||||
ChartMetaData: metadata.ChartMetaData{
|
||||
ProjectName: e.Resource.ExtendedInfo["projectName"].(string),
|
||||
ChartName: e.Resource.ExtendedInfo["chartName"].(string),
|
||||
Versions: e.Resource.Metadata.Artifacts[0].Tags,
|
||||
OccurAt: time.Now(),
|
||||
Operator: e.Resource.ExtendedInfo["operator"].(string),
|
||||
},
|
||||
}
|
||||
if err := event.Build(metaData); err == nil {
|
||||
if err := event.Publish(); err != nil {
|
||||
hlog.Errorf("failed to publish chart upload event: %v", err)
|
||||
}
|
||||
} else {
|
||||
hlog.Errorf("failed to build chart upload event metadata: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process downloading chart success webhook event
|
||||
if res.StatusCode == http.StatusOK {
|
||||
chartDownloadEvent := res.Request.Context().Value(common.ChartDownloadCtxKey)
|
||||
eventMetaData, ok := chartDownloadEvent.(*metadata.ChartDownloadMetaData)
|
||||
if ok && eventMetaData != nil {
|
||||
// Trigger harbor webhook
|
||||
event := &n_event.Event{}
|
||||
if err := event.Build(eventMetaData); err == nil {
|
||||
if err := event.Publish(); err != nil {
|
||||
hlog.Errorf("failed to publish chart download event: %v", err)
|
||||
}
|
||||
} else {
|
||||
hlog.Errorf("failed to build chart download event metadata: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Accept cases
|
||||
// Success or redirect
|
||||
if res.StatusCode >= http.StatusOK && res.StatusCode <= http.StatusTemporaryRedirect {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Detect the 401 code, if it is,overwrite it to 500.
|
||||
// We also re-write the error content to structural error object
|
||||
errorObj := make(map[string]string)
|
||||
if res.StatusCode == http.StatusUnauthorized {
|
||||
errorObj["error"] = "operation request from unauthorized source is rejected"
|
||||
res.StatusCode = http.StatusInternalServerError
|
||||
} else {
|
||||
// Extract the error and wrap it into the error object
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
errorObj["error"] = fmt.Sprintf("%s: %s", res.Status, err.Error())
|
||||
} else {
|
||||
if err := json.Unmarshal(data, &errorObj); err != nil {
|
||||
errorObj["error"] = string(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content, err := json.Marshal(errorObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
size := len(content)
|
||||
body := io.NopCloser(bytes.NewReader(content))
|
||||
res.Body = body
|
||||
res.ContentLength = int64(size)
|
||||
res.Header.Set(contentLengthHeader, strconv.Itoa(size))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Join the path
|
||||
// Copy from the go reverse proxy
|
||||
func singleJoiningSlash(a, b string) string {
|
||||
aslash := strings.HasSuffix(a, "/")
|
||||
bslash := strings.HasPrefix(b, "/")
|
||||
switch {
|
||||
case aslash && bslash:
|
||||
return a + b[1:]
|
||||
case !aslash && !bslash:
|
||||
return a + "/" + b
|
||||
}
|
||||
return a + b
|
||||
}
|
||||
|
||||
// Rewrite the incoming URL with the right backend URL pattern
|
||||
// Remove 'chartrepo' from the endpoints of manipulation API
|
||||
// Remove 'chartrepo' from the endpoints of repository services
|
||||
func rewriteURLPath(req *http.Request) {
|
||||
incomingURLPath := req.URL.Path
|
||||
|
||||
// Health check endpoint
|
||||
if incomingURLPath == chartRepoHealthEndpoint {
|
||||
req.URL.Path = "/health"
|
||||
return
|
||||
}
|
||||
|
||||
// Root uploading endpoint
|
||||
if incomingURLPath == rootUploadingEndpoint {
|
||||
req.URL.Path = strings.Replace(incomingURLPath, "chartrepo", defaultRepo, 1)
|
||||
return
|
||||
}
|
||||
|
||||
// Repository endpoints
|
||||
if strings.HasPrefix(incomingURLPath, "/chartrepo") {
|
||||
req.URL.Path = strings.TrimPrefix(incomingURLPath, "/chartrepo")
|
||||
return
|
||||
}
|
||||
|
||||
// API endpoints
|
||||
if strings.HasPrefix(incomingURLPath, "/api/chartrepo") {
|
||||
req.URL.Path = strings.Replace(incomingURLPath, "/chartrepo", "", 1)
|
||||
return
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test the URL rewrite function
|
||||
func TestURLRewrite(t *testing.T) {
|
||||
req, err := createRequest(http.MethodGet, "/api/chartrepo/health")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rewriteURLPath(req)
|
||||
if req.URL.Path != "/health" {
|
||||
t.Fatalf("Expect url format %s but got %s", "/health", req.URL.Path)
|
||||
}
|
||||
|
||||
req, err = createRequest(http.MethodGet, "/api/chartrepo/library/charts")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rewriteURLPath(req)
|
||||
if req.URL.Path != "/api/library/charts" {
|
||||
t.Fatalf("Expect url format %s but got %s", "/api/library/charts", req.URL.Path)
|
||||
}
|
||||
|
||||
req, err = createRequest(http.MethodPost, "/api/chartrepo/charts")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rewriteURLPath(req)
|
||||
if req.URL.Path != "/api/library/charts" {
|
||||
t.Fatalf("Expect url format %s but got %s", "/api/library/charts", req.URL.Path)
|
||||
}
|
||||
|
||||
req, err = createRequest(http.MethodGet, "/chartrepo/library/index.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rewriteURLPath(req)
|
||||
if req.URL.Path != "/library/index.yaml" {
|
||||
t.Fatalf("Expect url format %s but got %s", "/library/index.yaml", req.URL.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func createRequest(method string, url string) (*http.Request, error) {
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.RequestURI = url
|
||||
|
||||
return req, nil
|
||||
}
|
@ -1,141 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Extract error object '{"error": "****---***"}' from the content if existing
|
||||
// nil error will be returned if it does exist
|
||||
func extractError(content []byte) (text string, err error) {
|
||||
if len(content) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
errorObj := make(map[string]string)
|
||||
err = json.Unmarshal(content, &errorObj)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if errText, ok := errorObj["error"]; ok {
|
||||
return errText, nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Parse the redis configuration to the beego cache pattern
|
||||
// redis://:password@host:6379/1
|
||||
// redis+sentinel://anonymous:password@host1:26379,host2:26379/mymaster/1
|
||||
func parseRedisConfig(redisConfigV string) (map[string]string, error) {
|
||||
if len(redisConfigV) == 0 {
|
||||
return nil, errors.New("empty redis config")
|
||||
}
|
||||
|
||||
redisConfig := make(map[string]string)
|
||||
redisConfig["key"] = cacheCollectionName
|
||||
|
||||
if !strings.Contains(redisConfigV, "//") {
|
||||
redisConfigV = "redis://" + redisConfigV
|
||||
}
|
||||
u, err := url.Parse(redisConfigV)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bad _REDIS_URL:%s", redisConfigV)
|
||||
}
|
||||
if u.Scheme == "redis+sentinel" {
|
||||
ps := strings.Split(u.Path, "/")
|
||||
if len(ps) < 2 {
|
||||
return nil, fmt.Errorf("bad redis sentinel url: no master name, %s", redisConfigV)
|
||||
}
|
||||
if _, err := strconv.Atoi(ps[1]); err == nil {
|
||||
return nil, fmt.Errorf("bad redis sentinel url: master name should not be a number, %s", redisConfigV)
|
||||
}
|
||||
redisConfig["conn"] = u.Host
|
||||
|
||||
if u.User != nil {
|
||||
password, isSet := u.User.Password()
|
||||
if isSet {
|
||||
redisConfig["password"] = password
|
||||
}
|
||||
}
|
||||
if len(ps) > 2 {
|
||||
if _, err := strconv.Atoi(ps[2]); err != nil {
|
||||
return nil, fmt.Errorf("bad redis sentinel url: bad db, %s", redisConfigV)
|
||||
}
|
||||
redisConfig["dbNum"] = ps[2]
|
||||
} else {
|
||||
redisConfig["dbNum"] = "0"
|
||||
}
|
||||
redisConfig["masterName"] = ps[1]
|
||||
} else if u.Scheme == "redis" {
|
||||
redisConfig["conn"] = u.Host // host
|
||||
if u.User != nil {
|
||||
password, isSet := u.User.Password()
|
||||
if isSet {
|
||||
redisConfig["password"] = password
|
||||
}
|
||||
}
|
||||
if len(u.Path) > 1 {
|
||||
if _, err := strconv.Atoi(u.Path[1:]); err != nil {
|
||||
return nil, fmt.Errorf("bad redis url: bad db, %s", redisConfigV)
|
||||
}
|
||||
redisConfig["dbNum"] = u.Path[1:]
|
||||
} else {
|
||||
redisConfig["dbNum"] = "0"
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("bad redis scheme, %s", redisConfigV)
|
||||
}
|
||||
|
||||
return redisConfig, nil
|
||||
}
|
||||
|
||||
// What's the cache driver if it is set
|
||||
func parseCacheDriver() (string, bool) {
|
||||
driver, ok := os.LookupEnv(cacheDriverENVKey)
|
||||
return strings.ToLower(driver), ok
|
||||
}
|
||||
|
||||
// Get and parse the configuration for the chart cache
|
||||
func getCacheConfig() (*ChartCacheConfig, error) {
|
||||
driver, isSet := parseCacheDriver()
|
||||
if !isSet {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if driver != cacheDriverMem && driver != cacheDriverRedis {
|
||||
return nil, fmt.Errorf("cache driver '%s' is not supported, only support 'memory' and 'redis'", driver)
|
||||
}
|
||||
|
||||
if driver == cacheDriverMem {
|
||||
return &ChartCacheConfig{
|
||||
DriverType: driver,
|
||||
}, nil
|
||||
}
|
||||
|
||||
redisConfigV := os.Getenv(redisENVKey)
|
||||
redisCfg, err := parseRedisConfig(redisConfigV)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse redis configurations from '%s' with error: %s", redisCfg, err)
|
||||
}
|
||||
if _, isSet := redisCfg["masterName"]; isSet {
|
||||
driver = cacheDriverRedisSentinel
|
||||
}
|
||||
|
||||
// Convert config map to string
|
||||
cfgData, err := json.Marshal(redisCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse redis configurations from '%s' with error: %s", redisCfg, err)
|
||||
}
|
||||
|
||||
return &ChartCacheConfig{
|
||||
DriverType: driver,
|
||||
Config: string(cfgData),
|
||||
}, nil
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
package chartserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test the utility function parseRedisConfig
|
||||
func TestParseRedisConfig(t *testing.T) {
|
||||
// Case 1: empty addr
|
||||
redisAddr := ""
|
||||
if _, err := parseRedisConfig(redisAddr); err == nil {
|
||||
t.Fatal("expect non nil error but got nil one if addr is empty")
|
||||
}
|
||||
|
||||
// Case 2: short pattern, addr:port
|
||||
redisAddr = "redis:6379"
|
||||
if parsedConn, err := parseRedisConfig(redisAddr); err != nil {
|
||||
t.Fatalf("expect nil error but got non nil one if addr is short pattern: %s\n", parsedConn)
|
||||
}
|
||||
|
||||
// Case 3: long pattern but miss some parts
|
||||
redisAddr = "redis:6379?idle_timeout_seconds=100"
|
||||
if parsedConn, err := parseRedisConfig(redisAddr); err != nil {
|
||||
t.Fatalf("expect nil error but got non nil one if addr is long pattern with some parts missing: %v\n", parsedConn)
|
||||
} else {
|
||||
if num, ok := parsedConn["dbNum"]; !ok || num != "0" {
|
||||
t.Fatalf("expect 'dbNum:0' in the parsed conn str: %v\n", parsedConn)
|
||||
}
|
||||
}
|
||||
|
||||
// Case 4: long pattern
|
||||
redisAddr = ":Passw0rd@redis:6379/1?idle_timeout_seconds=100"
|
||||
if parsedConn, err := parseRedisConfig(redisAddr); err != nil {
|
||||
t.Fatal("expect nil error but got non nil one if addr is long pattern")
|
||||
} else {
|
||||
if num, ok := parsedConn["dbNum"]; !ok || num != "1" {
|
||||
t.Fatalf("expect 'dbNum:1' in the parsed conn str: %v", parsedConn)
|
||||
}
|
||||
if p, ok := parsedConn["password"]; !ok || p != "Passw0rd" {
|
||||
t.Fatalf("expect 'password:Passw0rd' in the parsed conn str: %v", parsedConn)
|
||||
}
|
||||
}
|
||||
|
||||
// Case 5: sentinel but miss master name
|
||||
redisAddr = "redis+sentinel://:Passw0rd@redis1:26379,redis2:26379/1?idle_timeout_seconds=100"
|
||||
if _, err := parseRedisConfig(redisAddr); err == nil {
|
||||
t.Fatal("expect no master name error but got nil")
|
||||
}
|
||||
|
||||
// Case 6: sentinel
|
||||
redisAddr = "redis+sentinel://:Passw0rd@redis1:26379,redis2:26379/mymaster/1?idle_timeout_seconds=100"
|
||||
if parsedConn, err := parseRedisConfig(redisAddr); err != nil {
|
||||
t.Fatal("expect nil error but got non nil one if addr is long pattern")
|
||||
} else {
|
||||
if num, ok := parsedConn["dbNum"]; !ok || num != "1" {
|
||||
t.Fatalf("expect 'dbNum:0' in the parsed conn str: %v", parsedConn)
|
||||
}
|
||||
if p, ok := parsedConn["password"]; !ok || p != "Passw0rd" {
|
||||
t.Fatalf("expect 'password:Passw0rd' in the parsed conn str: %v", parsedConn)
|
||||
}
|
||||
if v, ok := parsedConn["masterName"]; !ok || v != "mymaster" {
|
||||
t.Fatalf("expect 'masterName:mymaster' in the parsed conn str: %v", parsedConn)
|
||||
}
|
||||
if v, ok := parsedConn["conn"]; !ok || v != "redis1:26379,redis2:26379" {
|
||||
t.Fatalf("expect 'conn:redis1:26379,redis2:26379' in the parsed conn str: %v", parsedConn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCacheConfig(t *testing.T) {
|
||||
t.Run("no cache set", func(t *testing.T) {
|
||||
cacheConf, err := getCacheConfig()
|
||||
if err != nil || cacheConf != nil {
|
||||
t.Fatal("expect nil cache config and nil error but got non-nil one when parsing empty cache settings")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown cache type", func(t *testing.T) {
|
||||
t.Setenv(cacheDriverENVKey, "unknown")
|
||||
_, err := getCacheConfig()
|
||||
if err == nil {
|
||||
t.Fatal("expect non-nil error but got nil one when parsing unknown cache type")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("in memory cache type", func(t *testing.T) {
|
||||
t.Setenv(cacheDriverENVKey, cacheDriverMem)
|
||||
memCacheConf, err := getCacheConfig()
|
||||
if err != nil || memCacheConf == nil || memCacheConf.DriverType != cacheDriverMem {
|
||||
t.Fatal("expect in memory cache driver but got invalid one")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrong redis cache conf", func(t *testing.T) {
|
||||
t.Setenv(cacheDriverENVKey, cacheDriverRedis)
|
||||
t.Setenv(redisENVKey, "")
|
||||
_, err := getCacheConfig()
|
||||
if err == nil {
|
||||
t.Fatal("expect non-nil error but got nil one when parsing a invalid redis cache conf")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("redis cache conf", func(t *testing.T) {
|
||||
t.Setenv(cacheDriverENVKey, cacheDriverRedis)
|
||||
t.Setenv(redisENVKey, ":Passw0rd@redis:6379/1?idle_timeout_seconds=100")
|
||||
redisConf, err := getCacheConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("expect nil error but got non-nil one when parsing valid redis conf")
|
||||
}
|
||||
|
||||
if redisConf == nil || redisConf.DriverType != cacheDriverRedis {
|
||||
t.Fatal("expect redis cache driver but got invalid one")
|
||||
}
|
||||
|
||||
conf := make(map[string]string)
|
||||
if err = json.Unmarshal([]byte(redisConf.Config), &conf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if v, ok := conf["conn"]; !ok {
|
||||
t.Fatal("expect 'conn' filed in the parsed conf but got nothing")
|
||||
} else {
|
||||
if v != "redis:6379" {
|
||||
t.Fatalf("expect %s but got %s", "redis:6379", v)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
@ -46,7 +46,6 @@ const (
|
||||
ResourceTypeProject = "p"
|
||||
ResourceTypeRepository = "r"
|
||||
ResourceTypeImage = "i"
|
||||
ResourceTypeChart = "c"
|
||||
|
||||
ExtEndpoint = "ext_endpoint"
|
||||
AUTHMode = "auth_mode"
|
||||
@ -137,9 +136,6 @@ const (
|
||||
LDAPGroupAdminDn = "ldap_group_admin_dn"
|
||||
LDAPGroupMembershipAttribute = "ldap_group_membership_attribute"
|
||||
DefaultRegistryControllerEndpoint = "http://registryctl:8080"
|
||||
WithChartMuseum = "with_chartmuseum"
|
||||
ChartRepoURL = "chart_repository_url"
|
||||
DefaultChartRepoURL = "http://chartmuseum:9999"
|
||||
DefaultPortalURL = "http://portal:8080"
|
||||
DefaultRegistryCtlURL = "http://registryctl:8080"
|
||||
// Use this prefix to distinguish harbor user, the prefix contains a special character($), so it cannot be registered as a harbor user.
|
||||
@ -156,9 +152,6 @@ const (
|
||||
|
||||
AuthProxyRediretPath = "/c/authproxy/redirect"
|
||||
|
||||
ChartUploadCtxKey = contextKey("chart_upload_event")
|
||||
ChartDownloadCtxKey = contextKey("chart_download_event")
|
||||
|
||||
// Global notification enable configuration
|
||||
NotificationEnable = "notification_enable"
|
||||
|
||||
|
@ -35,32 +35,29 @@ const (
|
||||
|
||||
// const resource variables
|
||||
const (
|
||||
ResourceAll = Resource("*") // resource match any other resources
|
||||
ResourceConfiguration = Resource("configuration") // project configuration compatible for portal only
|
||||
ResourceHelmChart = Resource("helm-chart")
|
||||
ResourceHelmChartVersion = Resource("helm-chart-version")
|
||||
ResourceHelmChartVersionLabel = Resource("helm-chart-version-label")
|
||||
ResourceLabel = Resource("label")
|
||||
ResourceLog = Resource("log")
|
||||
ResourceLdapUser = Resource("ldap-user")
|
||||
ResourceMember = Resource("member")
|
||||
ResourceMetadata = Resource("metadata")
|
||||
ResourceQuota = Resource("quota")
|
||||
ResourceRepository = Resource("repository")
|
||||
ResourceTagRetention = Resource("tag-retention")
|
||||
ResourceImmutableTag = Resource("immutable-tag")
|
||||
ResourceRobot = Resource("robot")
|
||||
ResourceNotificationPolicy = Resource("notification-policy")
|
||||
ResourceScan = Resource("scan")
|
||||
ResourceScanner = Resource("scanner")
|
||||
ResourceArtifact = Resource("artifact")
|
||||
ResourceTag = Resource("tag")
|
||||
ResourceAccessory = Resource("accessory")
|
||||
ResourceArtifactAddition = Resource("artifact-addition")
|
||||
ResourceArtifactLabel = Resource("artifact-label")
|
||||
ResourcePreatPolicy = Resource("preheat-policy")
|
||||
ResourcePreatInstance = Resource("preheat-instance")
|
||||
ResourceSelf = Resource("") // subresource for self
|
||||
ResourceAll = Resource("*") // resource match any other resources
|
||||
ResourceConfiguration = Resource("configuration") // project configuration compatible for portal only
|
||||
ResourceLabel = Resource("label")
|
||||
ResourceLog = Resource("log")
|
||||
ResourceLdapUser = Resource("ldap-user")
|
||||
ResourceMember = Resource("member")
|
||||
ResourceMetadata = Resource("metadata")
|
||||
ResourceQuota = Resource("quota")
|
||||
ResourceRepository = Resource("repository")
|
||||
ResourceTagRetention = Resource("tag-retention")
|
||||
ResourceImmutableTag = Resource("immutable-tag")
|
||||
ResourceRobot = Resource("robot")
|
||||
ResourceNotificationPolicy = Resource("notification-policy")
|
||||
ResourceScan = Resource("scan")
|
||||
ResourceScanner = Resource("scanner")
|
||||
ResourceArtifact = Resource("artifact")
|
||||
ResourceTag = Resource("tag")
|
||||
ResourceAccessory = Resource("accessory")
|
||||
ResourceArtifactAddition = Resource("artifact-addition")
|
||||
ResourceArtifactLabel = Resource("artifact-label")
|
||||
ResourcePreatPolicy = Resource("preheat-policy")
|
||||
ResourcePreatInstance = Resource("preheat-instance")
|
||||
ResourceSelf = Resource("") // subresource for self
|
||||
|
||||
ResourceAuditLog = Resource("audit-log")
|
||||
ResourceCatalog = Resource("catalog")
|
||||
|
@ -68,20 +68,6 @@ var (
|
||||
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate}, // upload helm chart
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead}, // download helm chart
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate}, // upload helm chart version
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead}, // read and download helm chart version
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionUpdate},
|
||||
|
||||
@ -173,20 +159,6 @@ var (
|
||||
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
|
||||
@ -245,18 +217,6 @@ var (
|
||||
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionList},
|
||||
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionOperate},
|
||||
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
|
||||
@ -301,12 +261,6 @@ var (
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
|
||||
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
|
||||
@ -332,12 +286,6 @@ var (
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
|
||||
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
|
||||
|
@ -30,12 +30,6 @@ var (
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
|
||||
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
|
||||
|
||||
|
@ -122,7 +122,6 @@ func GetUnitTestConfig() map[string]interface{} {
|
||||
common.LDAPGroupSearchScope: 2,
|
||||
common.LDAPGroupAdminDn: "cn=harbor_users,ou=groups,dc=example,dc=com",
|
||||
common.WithNotary: "false",
|
||||
common.WithChartMuseum: "false",
|
||||
common.SelfRegistration: "true",
|
||||
common.WithTrivy: "true",
|
||||
common.TokenServiceURL: "http://core:8080/service/token",
|
||||
|
@ -44,7 +44,7 @@ type Processor interface {
|
||||
AbstractMetadata(ctx context.Context, artifact *artifact.Artifact, manifest []byte) error
|
||||
// AbstractAddition abstracts the addition of the artifact.
|
||||
// The additions are different for different artifacts:
|
||||
// build history for image; values.yaml, readme and dependencies for chart, etc
|
||||
// build history for image;
|
||||
AbstractAddition(ctx context.Context, artifact *artifact.Artifact, additionType string) (addition *Addition, err error)
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"github.com/goharbor/harbor/src/controller/event/handler/p2p"
|
||||
"github.com/goharbor/harbor/src/controller/event/handler/replication"
|
||||
"github.com/goharbor/harbor/src/controller/event/handler/webhook/artifact"
|
||||
"github.com/goharbor/harbor/src/controller/event/handler/webhook/chart"
|
||||
"github.com/goharbor/harbor/src/controller/event/handler/webhook/quota"
|
||||
"github.com/goharbor/harbor/src/controller/event/handler/webhook/scan"
|
||||
"github.com/goharbor/harbor/src/controller/event/metadata"
|
||||
@ -24,9 +23,6 @@ func init() {
|
||||
_ = notifier.Subscribe(event.TopicPushArtifact, &artifact.Handler{})
|
||||
_ = notifier.Subscribe(event.TopicPullArtifact, &artifact.Handler{})
|
||||
_ = notifier.Subscribe(event.TopicDeleteArtifact, &artifact.Handler{})
|
||||
_ = notifier.Subscribe(event.TopicUploadChart, &chart.Handler{})
|
||||
_ = notifier.Subscribe(event.TopicDeleteChart, &chart.Handler{})
|
||||
_ = notifier.Subscribe(event.TopicDownloadChart, &chart.Handler{})
|
||||
_ = notifier.Subscribe(event.TopicQuotaExceed, "a.Handler{})
|
||||
_ = notifier.Subscribe(event.TopicQuotaWarning, "a.Handler{})
|
||||
_ = notifier.Subscribe(event.TopicScanningFailed, &scan.Handler{})
|
||||
|
@ -21,11 +21,9 @@ const (
|
||||
EventTypeArtifactPush = "artifact_push"
|
||||
EventTypeArtifactDelete = "artifact_delete"
|
||||
EventTypeTagDelete = "tag_delete"
|
||||
EventTypeChartUpload = "chart_upload"
|
||||
EventTypeChartDelete = "chart_delete"
|
||||
)
|
||||
|
||||
// Event is the model that defines the image/chart pull/push event
|
||||
// Event is the model that defines the image pull/push event
|
||||
type Event struct {
|
||||
Type string
|
||||
Resource *model.Resource
|
||||
|
@ -37,8 +37,7 @@ func Handle(ctx context.Context, event *Event) error {
|
||||
var policies []*repctlmodel.Policy
|
||||
var err error
|
||||
switch event.Type {
|
||||
case EventTypeArtifactPush, EventTypeChartUpload, EventTypeTagDelete,
|
||||
EventTypeArtifactDelete, EventTypeChartDelete:
|
||||
case EventTypeArtifactPush, EventTypeTagDelete, EventTypeArtifactDelete:
|
||||
policies, err = getRelatedPolicies(ctx, event.Resource)
|
||||
default:
|
||||
return fmt.Errorf("unsupported event type %s", event.Type)
|
||||
|
@ -1,122 +0,0 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package chart
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/event"
|
||||
"github.com/goharbor/harbor/src/controller/event/handler/util"
|
||||
"github.com/goharbor/harbor/src/controller/project"
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/pkg/notification"
|
||||
"github.com/goharbor/harbor/src/pkg/notifier/model"
|
||||
proModels "github.com/goharbor/harbor/src/pkg/project/models"
|
||||
)
|
||||
|
||||
// Handler preprocess chart event data
|
||||
type Handler struct {
|
||||
}
|
||||
|
||||
// Name ...
|
||||
func (cph *Handler) Name() string {
|
||||
return "ChartWebhook"
|
||||
}
|
||||
|
||||
// Handle preprocess chart event data and then publish hook event
|
||||
func (cph *Handler) Handle(ctx context.Context, value interface{}) error {
|
||||
chartEvent, ok := value.(*event.ChartEvent)
|
||||
if !ok {
|
||||
return errors.New("invalid chart event type")
|
||||
}
|
||||
|
||||
if chartEvent == nil || len(chartEvent.Versions) == 0 || len(chartEvent.ProjectName) == 0 || len(chartEvent.ChartName) == 0 {
|
||||
return fmt.Errorf("data miss in chart event: %v", chartEvent)
|
||||
}
|
||||
|
||||
prj, err := project.Ctl.Get(ctx, chartEvent.ProjectName, project.Metadata(true))
|
||||
if err != nil {
|
||||
log.Errorf("failed to find project[%s] for chart event: %v", chartEvent.ProjectName, err)
|
||||
return err
|
||||
}
|
||||
policies, err := notification.PolicyMgr.GetRelatedPolices(ctx, prj.ProjectID, chartEvent.EventType)
|
||||
if err != nil {
|
||||
log.Errorf("failed to find policy for %s event: %v", chartEvent.EventType, err)
|
||||
return err
|
||||
}
|
||||
// if cannot find policy including event type in project, return directly
|
||||
if len(policies) == 0 {
|
||||
log.Debugf("cannot find policy for %s event: %v", chartEvent.EventType, chartEvent)
|
||||
return nil
|
||||
}
|
||||
|
||||
payload, err := constructChartPayload(chartEvent, prj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = util.SendHookWithPolicies(policies, payload, chartEvent.EventType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsStateful ...
|
||||
func (cph *Handler) IsStateful() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func constructChartPayload(event *event.ChartEvent, project *proModels.Project) (*model.Payload, error) {
|
||||
repoType := proModels.ProjectPrivate
|
||||
if project.IsPublic() {
|
||||
repoType = proModels.ProjectPublic
|
||||
}
|
||||
|
||||
payload := &model.Payload{
|
||||
Type: event.EventType,
|
||||
OccurAt: event.OccurAt.Unix(),
|
||||
EventData: &model.EventData{
|
||||
Repository: &model.Repository{
|
||||
Name: event.ChartName,
|
||||
Namespace: event.ProjectName,
|
||||
RepoFullName: fmt.Sprintf("%s/%s", event.ProjectName, event.ChartName),
|
||||
RepoType: repoType,
|
||||
},
|
||||
},
|
||||
Operator: event.Operator,
|
||||
}
|
||||
|
||||
extURL, err := config.ExtEndpoint()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get external endpoint failed: %v", err)
|
||||
}
|
||||
|
||||
resourcePrefix := fmt.Sprintf("%s/chartrepo/%s/charts/%s", extURL, event.ProjectName, event.ChartName)
|
||||
for _, v := range event.Versions {
|
||||
resURL := fmt.Sprintf("%s-%s.tgz", resourcePrefix, v)
|
||||
|
||||
resource := &model.Resource{
|
||||
Tag: v,
|
||||
ResourceURL: resURL,
|
||||
}
|
||||
payload.EventData.Resources = append(payload.EventData.Resources, resource)
|
||||
}
|
||||
return payload, nil
|
||||
}
|
@ -1,183 +0,0 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package chart
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
testutils "github.com/goharbor/harbor/src/common/utils/test"
|
||||
"github.com/goharbor/harbor/src/controller/event"
|
||||
"github.com/goharbor/harbor/src/controller/project"
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
_ "github.com/goharbor/harbor/src/pkg/config/db"
|
||||
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
|
||||
"github.com/goharbor/harbor/src/pkg/notification"
|
||||
"github.com/goharbor/harbor/src/pkg/notification/policy/model"
|
||||
proModels "github.com/goharbor/harbor/src/pkg/project/models"
|
||||
projecttesting "github.com/goharbor/harbor/src/testing/controller/project"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
testingnotification "github.com/goharbor/harbor/src/testing/pkg/notification/policy"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// do some initialization
|
||||
testutils.InitDatabaseFromEnv()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestChartPreprocessHandler_Handle(t *testing.T) {
|
||||
PolicyMgr := notification.PolicyMgr
|
||||
defer func() {
|
||||
notification.PolicyMgr = PolicyMgr
|
||||
}()
|
||||
policyMgrMock := &testingnotification.Manager{}
|
||||
notification.PolicyMgr = policyMgrMock
|
||||
|
||||
ProjectCtl := project.Ctl
|
||||
defer func() {
|
||||
project.Ctl = ProjectCtl
|
||||
}()
|
||||
projectCtl := &projecttesting.Controller{}
|
||||
project.Ctl = projectCtl
|
||||
|
||||
name := "project_for_test_chart_event_preprocess"
|
||||
mock.OnAnything(projectCtl, "Get").Return(func(ctx context.Context, projectIDOrName interface{}, options ...project.Option) *proModels.Project {
|
||||
return &proModels.Project{
|
||||
Name: name,
|
||||
OwnerID: 1,
|
||||
Metadata: map[string]string{
|
||||
proModels.ProMetaEnableContentTrust: "true",
|
||||
proModels.ProMetaPreventVul: "true",
|
||||
proModels.ProMetaSeverity: "Low",
|
||||
proModels.ProMetaReuseSysCVEAllowlist: "false",
|
||||
},
|
||||
}
|
||||
}, nil)
|
||||
projectCtl.On("Get")
|
||||
policyMgrMock.On("GetRelatedPolices", mock.Anything, mock.Anything, mock.Anything).Return([]*model.Policy{
|
||||
{
|
||||
ID: 1,
|
||||
EventTypes: []string{
|
||||
event.TopicUploadChart,
|
||||
event.TopicDownloadChart,
|
||||
event.TopicDeleteChart,
|
||||
event.TopicPushArtifact,
|
||||
event.TopicPullArtifact,
|
||||
event.TopicDeleteArtifact,
|
||||
event.TopicScanningFailed,
|
||||
event.TopicScanningCompleted,
|
||||
event.TopicQuotaExceed,
|
||||
},
|
||||
Targets: []model.EventTarget{
|
||||
{
|
||||
Type: "http",
|
||||
Address: "http://127.0.0.1:8080",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
handler := &Handler{}
|
||||
config.Init()
|
||||
|
||||
type args struct {
|
||||
data interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Handler Want Error 1",
|
||||
args: args{
|
||||
data: nil,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Handler Want Error 2",
|
||||
args: args{
|
||||
data: &event.ChartEvent{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Handler Want Error 3",
|
||||
args: args{
|
||||
data: &event.ChartEvent{
|
||||
Versions: []string{
|
||||
"v1.2.1",
|
||||
},
|
||||
ProjectName: "project_for_test_chart_event_preprocess",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Handler Want Error 4",
|
||||
args: args{
|
||||
data: &event.ChartEvent{
|
||||
Versions: []string{
|
||||
"v1.2.1",
|
||||
},
|
||||
ProjectName: "project_for_test_chart_event_preprocess_not_exists",
|
||||
ChartName: "testChart",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Handler Want Error 5",
|
||||
args: args{
|
||||
data: &event.ChartEvent{
|
||||
Versions: []string{
|
||||
"v1.2.1",
|
||||
},
|
||||
ProjectName: "project_for_test_chart_event_preprocess",
|
||||
ChartName: "testChart",
|
||||
EventType: "uploadChart",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := handler.Handle(context.TODO(), tt.args.data)
|
||||
if tt.wantErr {
|
||||
require.NotNil(t, err, "Error: %s", err)
|
||||
return
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartPreprocessHandler_IsStateful(t *testing.T) {
|
||||
handler := &Handler{}
|
||||
assert.False(t, handler.IsStateful())
|
||||
}
|
||||
|
||||
func TestChartPreprocessHandler_Name(t *testing.T) {
|
||||
handler := &Handler{}
|
||||
assert.Equal(t, "ChartWebhook", handler.Name())
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
event2 "github.com/goharbor/harbor/src/controller/event"
|
||||
"github.com/goharbor/harbor/src/pkg/notifier/event"
|
||||
)
|
||||
|
||||
// ChartMetaData defines meta data of chart event
|
||||
type ChartMetaData struct {
|
||||
ProjectName string
|
||||
ChartName string
|
||||
Versions []string
|
||||
OccurAt time.Time
|
||||
Operator string
|
||||
}
|
||||
|
||||
func (cmd *ChartMetaData) convert(evt *event2.ChartEvent) {
|
||||
evt.ProjectName = cmd.ProjectName
|
||||
evt.OccurAt = cmd.OccurAt
|
||||
evt.Operator = cmd.Operator
|
||||
evt.ChartName = cmd.ChartName
|
||||
evt.Versions = cmd.Versions
|
||||
}
|
||||
|
||||
// ChartUploadMetaData defines meta data of chart upload event
|
||||
type ChartUploadMetaData struct {
|
||||
ChartMetaData
|
||||
}
|
||||
|
||||
// Resolve chart uploading metadata into common chart event
|
||||
func (cu *ChartUploadMetaData) Resolve(event *event.Event) error {
|
||||
data := &event2.ChartEvent{
|
||||
EventType: event2.TopicUploadChart,
|
||||
}
|
||||
cu.convert(data)
|
||||
|
||||
event.Topic = event2.TopicUploadChart
|
||||
event.Data = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChartDownloadMetaData defines meta data of chart download event
|
||||
type ChartDownloadMetaData struct {
|
||||
ChartMetaData
|
||||
}
|
||||
|
||||
// Resolve chart download metadata into common chart event
|
||||
func (cd *ChartDownloadMetaData) Resolve(evt *event.Event) error {
|
||||
data := &event2.ChartEvent{
|
||||
EventType: event2.TopicDownloadChart,
|
||||
}
|
||||
cd.convert(data)
|
||||
|
||||
evt.Topic = event2.TopicDownloadChart
|
||||
evt.Data = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChartDeleteMetaData defines meta data of chart delete event
|
||||
type ChartDeleteMetaData struct {
|
||||
ChartMetaData
|
||||
}
|
||||
|
||||
// Resolve chart delete metadata into common chart event
|
||||
func (cd *ChartDeleteMetaData) Resolve(evt *event.Event) error {
|
||||
data := &event2.ChartEvent{
|
||||
EventType: event2.TopicDeleteChart,
|
||||
}
|
||||
cd.convert(data)
|
||||
|
||||
evt.Topic = event2.TopicDeleteChart
|
||||
evt.Data = data
|
||||
return nil
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
event2 "github.com/goharbor/harbor/src/controller/event"
|
||||
"github.com/goharbor/harbor/src/pkg/notifier/event"
|
||||
)
|
||||
|
||||
type chartEventTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (r *chartEventTestSuite) TestResolveOfUploadChartEventMetadata() {
|
||||
e := &event.Event{}
|
||||
metadata := &ChartUploadMetaData{
|
||||
ChartMetaData{
|
||||
ProjectName: "library",
|
||||
ChartName: "redis-v2.0",
|
||||
Versions: nil,
|
||||
OccurAt: time.Time{},
|
||||
Operator: "admin",
|
||||
},
|
||||
}
|
||||
err := metadata.Resolve(e)
|
||||
r.Require().Nil(err)
|
||||
r.Equal(event2.TopicUploadChart, e.Topic)
|
||||
r.Require().NotNil(e.Data)
|
||||
data, ok := e.Data.(*event2.ChartEvent)
|
||||
r.Require().True(ok)
|
||||
r.Equal("redis-v2.0", data.ChartName)
|
||||
}
|
||||
|
||||
func (r *chartEventTestSuite) TestResolveOfDownloadChartEventMetadata() {
|
||||
e := &event.Event{}
|
||||
metadata := &ChartDownloadMetaData{
|
||||
ChartMetaData{
|
||||
ProjectName: "library",
|
||||
ChartName: "redis-v2.0",
|
||||
Versions: nil,
|
||||
OccurAt: time.Time{},
|
||||
Operator: "admin",
|
||||
},
|
||||
}
|
||||
err := metadata.Resolve(e)
|
||||
r.Require().Nil(err)
|
||||
r.Equal(event2.TopicDownloadChart, e.Topic)
|
||||
r.Require().NotNil(e.Data)
|
||||
data, ok := e.Data.(*event2.ChartEvent)
|
||||
r.Require().True(ok)
|
||||
r.Equal("redis-v2.0", data.ChartName)
|
||||
}
|
||||
|
||||
func TestChartEventTestSuite(t *testing.T) {
|
||||
suite.Run(t, &chartEventTestSuite{})
|
||||
}
|
@ -44,9 +44,6 @@ const (
|
||||
// QuotaExceedTopic is topic for quota warning event, the usage reaches the warning bar of limitation, like 85%
|
||||
TopicQuotaWarning = "QUOTA_WARNING"
|
||||
TopicQuotaExceed = "QUOTA_EXCEED"
|
||||
TopicUploadChart = "UPLOAD_CHART"
|
||||
TopicDownloadChart = "DOWNLOAD_CHART"
|
||||
TopicDeleteChart = "DELETE_CHART"
|
||||
TopicReplication = "REPLICATION"
|
||||
TopicArtifactLabeled = "ARTIFACT_LABELED"
|
||||
TopicTagRetention = "TAG_RETENTION"
|
||||
@ -302,21 +299,6 @@ func (s *ScanImageEvent) String() string {
|
||||
s.Artifact, s.Operator, s.OccurAt.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
// ChartEvent is chart related event data to publish
|
||||
type ChartEvent struct {
|
||||
EventType string
|
||||
ProjectName string
|
||||
ChartName string
|
||||
Versions []string
|
||||
OccurAt time.Time
|
||||
Operator string
|
||||
}
|
||||
|
||||
func (c *ChartEvent) String() string {
|
||||
return fmt.Sprintf("ProjectName-%s ChartName-%s Versions-%s Operator-%s OccurAt-%s",
|
||||
c.ProjectName, c.ChartName, c.Versions, c.Operator, c.OccurAt.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
// QuotaEvent is project quota related event data to publish
|
||||
type QuotaEvent struct {
|
||||
EventType string
|
||||
|
@ -146,18 +146,6 @@ func registryCtlHealthChecker() health.Checker {
|
||||
return PeriodicHealthChecker(checker, period)
|
||||
}
|
||||
|
||||
func chartmuseumHealthChecker() health.Checker {
|
||||
url, err := config.GetChartMuseumEndpoint()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get the URL of chartmuseum: %v", err)
|
||||
}
|
||||
url = url + "/health"
|
||||
timeout := 60 * time.Second
|
||||
period := 10 * time.Second
|
||||
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
|
||||
return PeriodicHealthChecker(checker, period)
|
||||
}
|
||||
|
||||
func notaryHealthChecker() health.Checker {
|
||||
url := config.InternalNotaryEndpoint() + "/_notary_server/health"
|
||||
timeout := 60 * time.Second
|
||||
@ -203,9 +191,6 @@ func RegisterHealthCheckers() {
|
||||
registry["registryctl"] = registryCtlHealthChecker()
|
||||
registry["database"] = databaseHealthChecker()
|
||||
registry["redis"] = redisHealthChecker()
|
||||
if config.WithChartMuseum() {
|
||||
registry["chartmuseum"] = chartmuseumHealthChecker()
|
||||
}
|
||||
if config.WithNotary() {
|
||||
registry["notary"] = notaryHealthChecker()
|
||||
}
|
||||
|
@ -42,16 +42,6 @@ func (c *copyFlowTestSuite) TestRun() {
|
||||
},
|
||||
}, nil)
|
||||
adp.On("FetchArtifacts", mock.Anything).Return([]*model.Resource{
|
||||
{
|
||||
Type: model.ResourceTypeChart,
|
||||
Metadata: &model.ResourceMetadata{
|
||||
Repository: &model.Repository{
|
||||
Name: "library/hello-world",
|
||||
},
|
||||
Vtags: []string{"latest"},
|
||||
},
|
||||
Override: false,
|
||||
},
|
||||
{
|
||||
Type: model.ResourceTypeArtifact,
|
||||
Metadata: &model.ResourceMetadata{
|
||||
@ -61,7 +51,6 @@ func (c *copyFlowTestSuite) TestRun() {
|
||||
Vtags: []string{"latest"},
|
||||
},
|
||||
Override: false,
|
||||
Skip: true,
|
||||
},
|
||||
}, nil)
|
||||
adp.On("PrepareForPush", mock.Anything).Return(nil)
|
||||
|
@ -56,57 +56,16 @@ func initialize(policy *repctlmodel.Policy) (adp.Adapter, adp.Adapter, error) {
|
||||
|
||||
// fetch resources from the source registry
|
||||
func fetchResources(adapter adp.Adapter, policy *repctlmodel.Policy) ([]*model.Resource, error) {
|
||||
var resTypes []string
|
||||
for _, filter := range policy.Filters {
|
||||
if filter.Type == model.FilterTypeResource {
|
||||
resTypes = append(resTypes, filter.Value.(string))
|
||||
}
|
||||
}
|
||||
if len(resTypes) == 0 {
|
||||
info, err := adapter.Info()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get the adapter info: %v", err)
|
||||
}
|
||||
resTypes = append(resTypes, info.SupportedResourceTypes...)
|
||||
}
|
||||
|
||||
fetchArtifact := false
|
||||
fetchChart := false
|
||||
for _, resType := range resTypes {
|
||||
if resType == model.ResourceTypeChart {
|
||||
fetchChart = true
|
||||
continue
|
||||
}
|
||||
fetchArtifact = true
|
||||
}
|
||||
|
||||
var resources []*model.Resource
|
||||
// artifacts
|
||||
if fetchArtifact {
|
||||
reg, ok := adapter.(adp.ArtifactRegistry)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("the adapter doesn't implement the ArtifactRegistry interface")
|
||||
}
|
||||
res, err := reg.FetchArtifacts(policy.Filters)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch artifacts: %v", err)
|
||||
}
|
||||
resources = append(resources, res...)
|
||||
log.Debug("fetch artifacts completed")
|
||||
reg, ok := adapter.(adp.ArtifactRegistry)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("the adapter doesn't implement the ArtifactRegistry interface")
|
||||
}
|
||||
// charts
|
||||
if fetchChart {
|
||||
reg, ok := adapter.(adp.ChartRegistry)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("the adapter doesn't implement the ChartRegistry interface")
|
||||
}
|
||||
res, err := reg.FetchCharts(policy.Filters)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch charts: %v", err)
|
||||
}
|
||||
resources = append(resources, res...)
|
||||
log.Debug("fetch charts completed")
|
||||
res, err := reg.FetchArtifacts(policy.Filters)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch artifacts: %v", err)
|
||||
}
|
||||
resources = append(resources, res...)
|
||||
|
||||
log.Debug("fetch resources from the source registry completed")
|
||||
return resources, nil
|
||||
|
@ -53,11 +53,6 @@ func (s *stageTestSuite) TestInitialize() {
|
||||
|
||||
func (s *stageTestSuite) TestFetchResources() {
|
||||
adapter := &mockAdapter{}
|
||||
adapter.On("Info").Return(&model.RegistryInfo{
|
||||
SupportedResourceTypes: []string{
|
||||
model.ResourceTypeArtifact,
|
||||
},
|
||||
}, nil)
|
||||
adapter.On("FetchArtifacts", mock.Anything).Return([]*model.Resource{
|
||||
{},
|
||||
{},
|
||||
@ -72,7 +67,7 @@ func (s *stageTestSuite) TestFetchResources() {
|
||||
func (s *stageTestSuite) TestAssembleSourceResources() {
|
||||
resources := []*model.Resource{
|
||||
{
|
||||
Type: model.ResourceTypeChart,
|
||||
Type: model.ResourceTypeArtifact,
|
||||
Metadata: &model.ResourceMetadata{
|
||||
Repository: &model.Repository{
|
||||
Name: "library/hello-world",
|
||||
@ -95,7 +90,7 @@ func (s *stageTestSuite) TestAssembleSourceResources() {
|
||||
func (s *stageTestSuite) TestAssembleDestinationResources() {
|
||||
resources := []*model.Resource{
|
||||
{
|
||||
Type: model.ResourceTypeChart,
|
||||
Type: model.ResourceTypeArtifact,
|
||||
Metadata: &model.ResourceMetadata{
|
||||
Repository: &model.Repository{
|
||||
Name: "library/hello-world",
|
||||
@ -114,7 +109,7 @@ func (s *stageTestSuite) TestAssembleDestinationResources() {
|
||||
res, err := assembleDestinationResources(resources, policy, "")
|
||||
s.Require().Nil(err)
|
||||
s.Len(res, 1)
|
||||
s.Equal(model.ResourceTypeChart, res[0].Type)
|
||||
s.Equal(model.ResourceTypeArtifact, res[0].Type)
|
||||
s.Equal("test/hello-world", res[0].Metadata.Repository.Name)
|
||||
s.Equal(1, len(res[0].Metadata.Vtags))
|
||||
s.Equal("latest", res[0].Metadata.Vtags[0])
|
||||
|
@ -1,199 +0,0 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package chart
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
trans "github.com/goharbor/harbor/src/controller/replication/transfer"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/pkg/reg/adapter"
|
||||
"github.com/goharbor/harbor/src/pkg/reg/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := trans.RegisterFactory(model.ResourceTypeChart, factory); err != nil {
|
||||
log.Errorf("failed to register transfer factory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func factory(logger trans.Logger, stopFunc trans.StopFunc) (trans.Transfer, error) {
|
||||
return &transfer{
|
||||
logger: logger,
|
||||
isStopped: stopFunc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type chart struct {
|
||||
name string
|
||||
version string
|
||||
contentURL string
|
||||
}
|
||||
|
||||
type transfer struct {
|
||||
logger trans.Logger
|
||||
isStopped trans.StopFunc
|
||||
src adapter.ChartRegistry
|
||||
dst adapter.ChartRegistry
|
||||
}
|
||||
|
||||
func (t *transfer) Transfer(src *model.Resource, dst *model.Resource, opts *trans.Options) error {
|
||||
// initialize
|
||||
if err := t.initialize(src, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete the chart on destination registry
|
||||
if dst.Deleted {
|
||||
return t.delete(&chart{
|
||||
name: dst.Metadata.Repository.Name,
|
||||
version: dst.Metadata.Artifacts[0].Tags[0],
|
||||
})
|
||||
}
|
||||
|
||||
var contentURL string
|
||||
if len(src.ExtendedInfo) > 0 && src.ExtendedInfo["contentURL"] != nil {
|
||||
contentURL = src.ExtendedInfo["contentURL"].(string)
|
||||
}
|
||||
|
||||
srcChart := &chart{
|
||||
name: src.Metadata.Repository.Name,
|
||||
version: src.Metadata.Artifacts[0].Tags[0],
|
||||
contentURL: contentURL,
|
||||
}
|
||||
dstChart := &chart{
|
||||
name: dst.Metadata.Repository.Name,
|
||||
version: dst.Metadata.Artifacts[0].Tags[0],
|
||||
}
|
||||
// copy the chart from source registry to the destination
|
||||
return t.copy(srcChart, dstChart, dst.Override, opts)
|
||||
}
|
||||
|
||||
func (t *transfer) initialize(src, dst *model.Resource) error {
|
||||
// create client for source registry
|
||||
srcReg, err := createRegistry(src.Registry)
|
||||
if err != nil {
|
||||
t.logger.Errorf("failed to create client for source registry: %v", err)
|
||||
return err
|
||||
}
|
||||
t.src = srcReg
|
||||
t.logger.Infof("client for source registry [type: %s, URL: %s, insecure: %v] created",
|
||||
src.Registry.Type, src.Registry.URL, src.Registry.Insecure)
|
||||
|
||||
// create client for destination registry
|
||||
dstReg, err := createRegistry(dst.Registry)
|
||||
if err != nil {
|
||||
t.logger.Errorf("failed to create client for destination registry: %v", err)
|
||||
return err
|
||||
}
|
||||
t.dst = dstReg
|
||||
t.logger.Infof("client for destination registry [type: %s, URL: %s, insecure: %v] created",
|
||||
dst.Registry.Type, dst.Registry.URL, dst.Registry.Insecure)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createRegistry(reg *model.Registry) (adapter.ChartRegistry, error) {
|
||||
factory, err := adapter.GetFactory(reg.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ad, err := factory.Create(reg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
registry, ok := ad.(adapter.ChartRegistry)
|
||||
if !ok {
|
||||
return nil, errors.New("the adapter doesn't implement the \"ChartRegistry\" interface")
|
||||
}
|
||||
return registry, nil
|
||||
}
|
||||
|
||||
func (t *transfer) shouldStop() bool {
|
||||
isStopped := t.isStopped()
|
||||
if isStopped {
|
||||
t.logger.Info("the job is stopped")
|
||||
}
|
||||
return isStopped
|
||||
}
|
||||
|
||||
func (t *transfer) copy(src, dst *chart, override bool, opts *trans.Options) error {
|
||||
if t.shouldStop() {
|
||||
return nil
|
||||
}
|
||||
t.logger.Infof("copying %s:%s(source registry) to %s:%s(destination registry)...",
|
||||
src.name, src.version, dst.name, dst.version)
|
||||
|
||||
// check the existence of the chart on the destination registry
|
||||
exist, err := t.dst.ChartExist(dst.name, dst.version)
|
||||
if err != nil {
|
||||
t.logger.Errorf("failed to check the existence of chart %s:%s on the destination registry: %v", dst.name, dst.version, err)
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
// the same name chart exists, but not allowed to override
|
||||
if !override {
|
||||
t.logger.Warningf("the same name chart %s:%s exists on the destination registry, but the \"override\" is set to false, skip",
|
||||
dst.name, dst.version)
|
||||
return nil
|
||||
}
|
||||
// the same name chart exists, but allowed to override
|
||||
t.logger.Warningf("the same name chart %s:%s exists on the destination registry and the \"override\" is set to true, continue...",
|
||||
dst.name, dst.version)
|
||||
}
|
||||
|
||||
// copy the chart between the source and destination registries
|
||||
chart, err := t.src.DownloadChart(src.name, src.version, src.contentURL)
|
||||
if err != nil {
|
||||
t.logger.Errorf("failed to download the chart %s:%s: %v", src.name, src.version, err)
|
||||
return err
|
||||
}
|
||||
if opts.Speed > 0 {
|
||||
t.logger.Infof("limit network speed at %d kb/s", opts.Speed)
|
||||
chart = trans.NewReader(chart, opts.Speed)
|
||||
}
|
||||
defer chart.Close()
|
||||
|
||||
if err = t.dst.UploadChart(dst.name, dst.version, chart); err != nil {
|
||||
t.logger.Errorf("failed to upload the chart %s:%s: %v", dst.name, dst.version, err)
|
||||
return err
|
||||
}
|
||||
|
||||
t.logger.Infof("copy %s:%s(source registry) to %s:%s(destination registry) completed",
|
||||
src.name, src.version, dst.name, dst.version)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *transfer) delete(chart *chart) error {
|
||||
exist, err := t.dst.ChartExist(chart.name, chart.version)
|
||||
if err != nil {
|
||||
t.logger.Errorf("failed to check the existence of chart %s:%s on the destination registry: %v", chart.name, chart.version, err)
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
t.logger.Infof("the chart %s:%s doesn't exist on the destination registry, skip",
|
||||
chart.name, chart.version)
|
||||
return nil
|
||||
}
|
||||
|
||||
t.logger.Infof("deleting the chart %s:%s on the destination registry...", chart.name, chart.version)
|
||||
if err := t.dst.DeleteChart(chart.name, chart.version); err != nil {
|
||||
t.logger.Errorf("failed to delete the chart %s:%s on the destination registry: %v", chart.name, chart.version, err)
|
||||
return err
|
||||
}
|
||||
t.logger.Infof("delete the chart %s:%s on the destination registry completed", chart.name, chart.version)
|
||||
return nil
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package chart
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
trans "github.com/goharbor/harbor/src/controller/replication/transfer"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/pkg/reg/model"
|
||||
)
|
||||
|
||||
type fakeRegistry struct{}
|
||||
|
||||
func (f *fakeRegistry) FetchCharts(filters []*model.Filter) ([]*model.Resource, error) {
|
||||
return []*model.Resource{
|
||||
{
|
||||
Type: model.ResourceTypeChart,
|
||||
Metadata: &model.ResourceMetadata{
|
||||
Repository: &model.Repository{
|
||||
Name: "library/harbor",
|
||||
},
|
||||
Vtags: []string{"0.2.0"},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
func (f *fakeRegistry) ChartExist(name, version string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
func (f *fakeRegistry) DownloadChart(name, version, contentURL string) (io.ReadCloser, error) {
|
||||
r := io.NopCloser(bytes.NewReader([]byte{'a'}))
|
||||
return r, nil
|
||||
}
|
||||
func (f *fakeRegistry) UploadChart(name, version string, chart io.Reader) error {
|
||||
return nil
|
||||
}
|
||||
func (f *fakeRegistry) DeleteChart(name, version string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestFactory(t *testing.T) {
|
||||
tr, err := factory(nil, nil)
|
||||
require.Nil(t, err)
|
||||
_, ok := tr.(trans.Transfer)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestShouldStop(t *testing.T) {
|
||||
// should stop
|
||||
stopFunc := func() bool { return true }
|
||||
tr := &transfer{
|
||||
logger: log.DefaultLogger(),
|
||||
isStopped: stopFunc,
|
||||
}
|
||||
assert.True(t, tr.shouldStop())
|
||||
|
||||
// should not stop
|
||||
stopFunc = func() bool { return false }
|
||||
tr = &transfer{
|
||||
isStopped: stopFunc,
|
||||
}
|
||||
assert.False(t, tr.shouldStop())
|
||||
}
|
||||
|
||||
func TestCopy(t *testing.T) {
|
||||
stopFunc := func() bool { return false }
|
||||
transfer := &transfer{
|
||||
logger: log.DefaultLogger(),
|
||||
isStopped: stopFunc,
|
||||
src: &fakeRegistry{},
|
||||
dst: &fakeRegistry{},
|
||||
}
|
||||
src := &chart{
|
||||
name: "library/harbor",
|
||||
version: "0.2.0",
|
||||
}
|
||||
dst := &chart{
|
||||
name: "dest/harbor",
|
||||
version: "0.2.0",
|
||||
}
|
||||
err := transfer.copy(src, dst, true, trans.NewOptions())
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
stopFunc := func() bool { return false }
|
||||
transfer := &transfer{
|
||||
logger: log.DefaultLogger(),
|
||||
isStopped: stopFunc,
|
||||
src: &fakeRegistry{},
|
||||
dst: &fakeRegistry{},
|
||||
}
|
||||
chart := &chart{
|
||||
name: "dest/harbor",
|
||||
version: "0.2.0",
|
||||
}
|
||||
err := transfer.delete(chart)
|
||||
assert.Nil(t, err)
|
||||
}
|
@ -60,7 +60,6 @@ type protectedData struct {
|
||||
HasCARoot bool
|
||||
RegistryStorageProviderName string
|
||||
ReadOnly bool
|
||||
WithChartMuseum bool
|
||||
NotificationEnable bool
|
||||
}
|
||||
|
||||
@ -121,7 +120,6 @@ func (c *controller) GetInfo(ctx context.Context, opt Options) (*Data, error) {
|
||||
res.Protected = &protectedData{
|
||||
CurrentTime: time.Now(),
|
||||
WithNotary: config.WithNotary(),
|
||||
WithChartMuseum: config.WithChartMuseum(),
|
||||
ReadOnly: config.ReadOnly(ctx),
|
||||
ExtURL: extURL,
|
||||
RegistryURL: registryURL,
|
||||
|
@ -31,7 +31,6 @@ func (s *sysInfoCtlTestSuite) SetupTest() {
|
||||
common.RegistryStorageProviderName: "filesystem",
|
||||
common.ReadOnly: false,
|
||||
common.NotificationEnable: false,
|
||||
common.WithChartMuseum: false,
|
||||
common.WithNotary: true,
|
||||
}
|
||||
|
||||
@ -77,7 +76,6 @@ func (s *sysInfoCtlTestSuite) TestGetInfo() {
|
||||
HasCARoot: true,
|
||||
RegistryStorageProviderName: "filesystem",
|
||||
ReadOnly: false,
|
||||
WithChartMuseum: false,
|
||||
NotificationEnable: false,
|
||||
},
|
||||
},
|
||||
|
@ -19,7 +19,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -29,7 +28,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/goharbor/harbor/src/chartserver"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
common_http "github.com/goharbor/harbor/src/common/http"
|
||||
@ -38,7 +36,6 @@ import (
|
||||
"github.com/goharbor/harbor/src/pkg/member"
|
||||
memberModels "github.com/goharbor/harbor/src/pkg/member/models"
|
||||
"github.com/goharbor/harbor/src/pkg/user"
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -315,25 +312,3 @@ func clean() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Provides a mock chart controller for deletable test cases
|
||||
func mockChartController() (*httptest.Server, *chartserver.Controller, error) {
|
||||
mockServer := httptest.NewServer(htesting.MockChartRepoHandler)
|
||||
|
||||
var oldController, newController *chartserver.Controller
|
||||
url, err := url.Parse(mockServer.URL)
|
||||
if err == nil {
|
||||
newController, err = chartserver.NewController(url)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
mockServer.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Override current controller and keep the old one for restoring
|
||||
oldController = chartController
|
||||
chartController = newController
|
||||
|
||||
return mockServer, oldController, nil
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/controller/p2p/preheat"
|
||||
projectcontroller "github.com/goharbor/harbor/src/controller/project"
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/pkg/scheduler"
|
||||
@ -175,11 +174,6 @@ func (b *BaseController) PopulateUserSession(u models.User) {
|
||||
|
||||
// Init related objects/configurations for the API controllers
|
||||
func Init() error {
|
||||
// init chart controller
|
||||
if err := initChartController(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p2pPreheatCallbackFun := func(ctx context.Context, p string) error {
|
||||
param := &preheat.TriggerParam{}
|
||||
if err := json.Unmarshal([]byte(p), param); err != nil {
|
||||
@ -192,18 +186,3 @@ func Init() error {
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func initChartController() error {
|
||||
// If chart repository is not enabled then directly return
|
||||
if !config.WithChartMuseum() {
|
||||
return nil
|
||||
}
|
||||
|
||||
chartCtl, err := initializeChartController()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chartController = chartCtl
|
||||
return nil
|
||||
}
|
||||
|
@ -1,114 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/pkg/label/model"
|
||||
proModels "github.com/goharbor/harbor/src/pkg/project/models"
|
||||
)
|
||||
|
||||
const (
|
||||
versionParam = ":version"
|
||||
idParam = ":id"
|
||||
)
|
||||
|
||||
// ChartLabelAPI handles the requests of marking/removing labels to/from charts.
|
||||
type ChartLabelAPI struct {
|
||||
LabelResourceAPI
|
||||
project *proModels.Project
|
||||
chartFullName string
|
||||
}
|
||||
|
||||
// Prepare required material for follow-up actions.
|
||||
func (cla *ChartLabelAPI) Prepare() {
|
||||
// Super
|
||||
cla.LabelResourceAPI.Prepare()
|
||||
|
||||
// Check authorization
|
||||
if !cla.SecurityCtx.IsAuthenticated() {
|
||||
cla.SendUnAuthorizedError(errors.New("UnAuthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
project := cla.GetStringFromPath(namespaceParam)
|
||||
|
||||
// Project should be a valid existing one
|
||||
existingProject, err := cla.ProjectCtl.Get(cla.Context(), project)
|
||||
if err != nil {
|
||||
cla.SendError(err)
|
||||
return
|
||||
}
|
||||
cla.project = existingProject
|
||||
|
||||
// Check the existence of target chart
|
||||
chartName := cla.GetStringFromPath(nameParam)
|
||||
version := cla.GetStringFromPath(versionParam)
|
||||
|
||||
if _, err = chartController.GetChartVersion(project, chartName, version); err != nil {
|
||||
cla.SendNotFoundError(err)
|
||||
return
|
||||
}
|
||||
cla.chartFullName = fmt.Sprintf("%s/%s:%s", project, chartName, version)
|
||||
}
|
||||
|
||||
func (cla *ChartLabelAPI) requireAccess(action rbac.Action) bool {
|
||||
return cla.RequireProjectAccess(cla.project.ProjectID, action, rbac.ResourceHelmChartVersionLabel)
|
||||
}
|
||||
|
||||
// MarkLabel handles the request of marking label to chart.
|
||||
func (cla *ChartLabelAPI) MarkLabel() {
|
||||
if !cla.requireAccess(rbac.ActionCreate) {
|
||||
return
|
||||
}
|
||||
|
||||
l := &model.Label{}
|
||||
if err := cla.DecodeJSONReq(l); err != nil {
|
||||
cla.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
|
||||
label, ok := cla.validate(l.ID, cla.project.ProjectID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
label2Res := &models.ResourceLabel{
|
||||
LabelID: label.ID,
|
||||
ResourceType: common.ResourceTypeChart,
|
||||
ResourceName: cla.chartFullName,
|
||||
}
|
||||
|
||||
cla.markLabelToResource(label2Res)
|
||||
}
|
||||
|
||||
// RemoveLabel handles the request of removing label from chart.
|
||||
func (cla *ChartLabelAPI) RemoveLabel() {
|
||||
if !cla.requireAccess(rbac.ActionDelete) {
|
||||
return
|
||||
}
|
||||
|
||||
lID, err := cla.GetInt64FromPath(idParam)
|
||||
if err != nil {
|
||||
cla.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
label, ok := cla.exists(lID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
cla.removeLabelFromResource(common.ResourceTypeChart, cla.chartFullName, label.ID)
|
||||
}
|
||||
|
||||
// GetLabels gets labels for the specified chart version.
|
||||
func (cla *ChartLabelAPI) GetLabels() {
|
||||
if !cla.requireAccess(rbac.ActionList) {
|
||||
return
|
||||
}
|
||||
cla.getLabelsOfResource(common.ResourceTypeChart, cla.chartFullName)
|
||||
}
|
@ -1,226 +0,0 @@
|
||||
// Copyright 2018 Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/goharbor/harbor/src/chartserver"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/api"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
pkg_dao "github.com/goharbor/harbor/src/pkg/label/dao"
|
||||
"github.com/goharbor/harbor/src/pkg/label/model"
|
||||
)
|
||||
|
||||
var (
|
||||
resourceLabelAPIPath = fmt.Sprintf("/api/%s/chartrepo/library/charts/harbor/0.2.0/labels", api.APIVersion)
|
||||
resourceLabelAPIPathWithFakeProject = fmt.Sprintf("/api/%s/chartrepo/not-exist/charts/harbor/0.2.0/labels", api.APIVersion)
|
||||
resourceLabelAPIPathWithFakeChart = fmt.Sprintf("/api/%s/chartrepo/library/charts/not-exist/0.2.0/labels", api.APIVersion)
|
||||
cProLibraryLabelID int64
|
||||
mockChartServer *httptest.Server
|
||||
oldChartController *chartserver.Controller
|
||||
labelDao pkg_dao.DAO
|
||||
)
|
||||
|
||||
func TestToStartMockChartService(t *testing.T) {
|
||||
var err error
|
||||
mockChartServer, oldChartController, err = mockChartController()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to start the mock chart service: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAddToChart(t *testing.T) {
|
||||
labelDao = pkg_dao.New()
|
||||
cSysLevelLabelID, err := labelDao.Create(orm.Context(), &model.Label{
|
||||
Name: "c_sys_level_label",
|
||||
Level: common.LabelLevelSystem,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer labelDao.Delete(orm.Context(), cSysLevelLabelID)
|
||||
|
||||
cProTestLabelID, err := labelDao.Create(orm.Context(), &model.Label{
|
||||
Name: "c_pro_test_label",
|
||||
Level: common.LabelLevelUser,
|
||||
Scope: common.LabelScopeProject,
|
||||
ProjectID: 100,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer labelDao.Delete(orm.Context(), cProTestLabelID)
|
||||
|
||||
cProLibraryLabelID, err = labelDao.Create(orm.Context(), &model.Label{
|
||||
Name: "c_pro_library_label",
|
||||
Level: common.LabelLevelUser,
|
||||
Scope: common.LabelScopeProject,
|
||||
ProjectID: 1,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
cases := []*codeCheckingCase{
|
||||
// 401
|
||||
{
|
||||
request: &testingRequest{
|
||||
url: resourceLabelAPIPath,
|
||||
method: http.MethodPost,
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
// 403
|
||||
{
|
||||
request: &testingRequest{
|
||||
url: resourceLabelAPIPath,
|
||||
method: http.MethodPost,
|
||||
credential: projGuest,
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
// 500 project doesn't exist
|
||||
{
|
||||
request: &testingRequest{
|
||||
url: resourceLabelAPIPathWithFakeProject,
|
||||
method: http.MethodPost,
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
},
|
||||
// 404 chart doesn't exist
|
||||
{
|
||||
request: &testingRequest{
|
||||
url: resourceLabelAPIPathWithFakeChart,
|
||||
method: http.MethodPost,
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
},
|
||||
// 400
|
||||
{
|
||||
request: &testingRequest{
|
||||
url: resourceLabelAPIPath,
|
||||
method: http.MethodPost,
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 404 label doesn't exist
|
||||
{
|
||||
request: &testingRequest{
|
||||
url: resourceLabelAPIPath,
|
||||
method: http.MethodPost,
|
||||
credential: projDeveloper,
|
||||
bodyJSON: struct {
|
||||
ID int64
|
||||
}{
|
||||
ID: 1000,
|
||||
},
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
},
|
||||
// 400 system level label
|
||||
{
|
||||
request: &testingRequest{
|
||||
url: resourceLabelAPIPath,
|
||||
method: http.MethodPost,
|
||||
credential: projDeveloper,
|
||||
bodyJSON: struct {
|
||||
ID int64
|
||||
}{
|
||||
ID: cSysLevelLabelID,
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 400 try to add the label of project1 to the image under project2
|
||||
{
|
||||
request: &testingRequest{
|
||||
url: resourceLabelAPIPath,
|
||||
method: http.MethodPost,
|
||||
credential: projDeveloper,
|
||||
bodyJSON: struct {
|
||||
ID int64
|
||||
}{
|
||||
ID: cProTestLabelID,
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
url: resourceLabelAPIPath,
|
||||
method: http.MethodPost,
|
||||
credential: projDeveloper,
|
||||
bodyJSON: struct {
|
||||
ID int64
|
||||
}{
|
||||
ID: cProLibraryLabelID,
|
||||
},
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
}
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
||||
|
||||
func TestGetOfChart(t *testing.T) {
|
||||
labels := []*model.Label{}
|
||||
err := handleAndParse(&testingRequest{
|
||||
url: resourceLabelAPIPath,
|
||||
method: http.MethodGet,
|
||||
credential: projDeveloper,
|
||||
}, &labels)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(labels))
|
||||
assert.Equal(t, cProLibraryLabelID, labels[0].ID)
|
||||
}
|
||||
|
||||
func TestRemoveFromChart(t *testing.T) {
|
||||
runCodeCheckingCases(t, &codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: fmt.Sprintf("%s/%d", resourceLabelAPIPath, cProLibraryLabelID),
|
||||
method: http.MethodDelete,
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
})
|
||||
|
||||
labels := []*model.Label{}
|
||||
err := handleAndParse(&testingRequest{
|
||||
url: resourceLabelAPIPath,
|
||||
method: http.MethodGet,
|
||||
credential: projDeveloper,
|
||||
}, &labels)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 0, len(labels))
|
||||
}
|
||||
|
||||
func TestToStopMockChartService(t *testing.T) {
|
||||
if mockChartServer != nil {
|
||||
mockChartServer.Close()
|
||||
}
|
||||
|
||||
if oldChartController != nil {
|
||||
chartController = oldChartController
|
||||
}
|
||||
labelDao = pkg_dao.New()
|
||||
labelDao.Delete(orm.Context(), cProLibraryLabelID)
|
||||
}
|
@ -1,651 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/chartserver"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
rep_event "github.com/goharbor/harbor/src/controller/event/handler/replication/event"
|
||||
"github.com/goharbor/harbor/src/controller/event/metadata"
|
||||
"github.com/goharbor/harbor/src/controller/project"
|
||||
"github.com/goharbor/harbor/src/core/label"
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
hlog "github.com/goharbor/harbor/src/lib/log"
|
||||
pkg_label "github.com/goharbor/harbor/src/pkg/label"
|
||||
n_event "github.com/goharbor/harbor/src/pkg/notifier/event"
|
||||
"github.com/goharbor/harbor/src/pkg/reg/model"
|
||||
"github.com/goharbor/harbor/src/server/middleware/orm"
|
||||
)
|
||||
|
||||
const (
|
||||
namespaceParam = ":repo"
|
||||
nameParam = ":name"
|
||||
filenameParam = ":filename"
|
||||
defaultRepo = "library"
|
||||
rootUploadingEndpoint = "/api/chartrepo/charts"
|
||||
rootIndexEndpoint = "/chartrepo/index.yaml"
|
||||
chartRepoHealthEndpoint = "/api/chartrepo/health"
|
||||
|
||||
formFieldNameForChart = "chart"
|
||||
formFiledNameForProv = "prov"
|
||||
headerContentType = "Content-Type"
|
||||
contentTypeMultipart = "multipart/form-data"
|
||||
// chartPackageFileExtension is the file extension used for chart packages
|
||||
chartPackageFileExtension = "tgz"
|
||||
)
|
||||
|
||||
// chartController is a singleton instance
|
||||
var chartController *chartserver.Controller
|
||||
|
||||
// GetChartController returns the chart controller
|
||||
func GetChartController() *chartserver.Controller {
|
||||
return chartController
|
||||
}
|
||||
|
||||
// ChartRepositoryAPI provides related API handlers for the chart repository APIs
|
||||
type ChartRepositoryAPI struct {
|
||||
// The base controller to provide common utilities
|
||||
BaseController
|
||||
|
||||
// For label management
|
||||
labelManager *label.BaseManager
|
||||
|
||||
// Keep the namespace if existing
|
||||
namespace string
|
||||
}
|
||||
|
||||
// Prepare something for the following actions
|
||||
func (cra *ChartRepositoryAPI) Prepare() {
|
||||
// Call super prepare method
|
||||
cra.BaseController.Prepare()
|
||||
|
||||
// Try to extract namespace for parameter of path
|
||||
// It may not exist
|
||||
cra.namespace = strings.TrimSpace(cra.GetStringFromPath(namespaceParam))
|
||||
|
||||
// Check the existence of namespace
|
||||
// Exclude the following URI
|
||||
// -/index.yaml
|
||||
// -/api/chartserver/health
|
||||
incomingURI := cra.Ctx.Request.URL.Path
|
||||
if incomingURI == rootUploadingEndpoint {
|
||||
// Forward to the default repository
|
||||
cra.namespace = defaultRepo
|
||||
}
|
||||
|
||||
if incomingURI != rootIndexEndpoint &&
|
||||
incomingURI != chartRepoHealthEndpoint {
|
||||
if !cra.requireNamespace(cra.namespace) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Init label manager
|
||||
cra.labelManager = &label.BaseManager{
|
||||
LabelMgr: pkg_label.Mgr,
|
||||
}
|
||||
}
|
||||
|
||||
func (cra *ChartRepositoryAPI) requireAccess(action rbac.Action, subresource ...rbac.Resource) bool {
|
||||
if len(subresource) == 0 {
|
||||
subresource = append(subresource, rbac.ResourceHelmChart)
|
||||
}
|
||||
|
||||
return cra.RequireProjectAccess(cra.namespace, action, subresource...)
|
||||
}
|
||||
|
||||
// GetHealthStatus handles GET /api/chartrepo/health
|
||||
func (cra *ChartRepositoryAPI) GetHealthStatus() {
|
||||
// Check access
|
||||
if !cra.SecurityCtx.IsAuthenticated() {
|
||||
cra.SendUnAuthorizedError(errors.New("unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
if !cra.SecurityCtx.IsSysAdmin() {
|
||||
cra.SendForbiddenError(errors.New(cra.SecurityCtx.GetUsername()))
|
||||
return
|
||||
}
|
||||
|
||||
// Directly proxy to the backend
|
||||
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
||||
}
|
||||
|
||||
// GetIndexByRepo handles GET /:repo/index.yaml
|
||||
func (cra *ChartRepositoryAPI) GetIndexByRepo() {
|
||||
// Check access
|
||||
if !cra.requireAccess(rbac.ActionRead) {
|
||||
return
|
||||
}
|
||||
|
||||
// Directly proxy to the backend
|
||||
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
||||
}
|
||||
|
||||
// GetIndex handles GET /index.yaml
|
||||
func (cra *ChartRepositoryAPI) GetIndex() {
|
||||
// Check access
|
||||
if !cra.SecurityCtx.IsAuthenticated() {
|
||||
cra.SendUnAuthorizedError(errors.New("unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
if !cra.SecurityCtx.IsSysAdmin() {
|
||||
cra.SendForbiddenError(errors.New(cra.SecurityCtx.GetUsername()))
|
||||
return
|
||||
}
|
||||
|
||||
projects, err := cra.ProjectCtl.List(cra.Context(), nil, project.Metadata(false))
|
||||
if err != nil {
|
||||
cra.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
namespaces := []string{}
|
||||
for _, r := range projects {
|
||||
namespaces = append(namespaces, r.Name)
|
||||
}
|
||||
|
||||
indexFile, err := chartController.GetIndexFile(namespaces)
|
||||
if err != nil {
|
||||
cra.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
cra.WriteYamlData(indexFile)
|
||||
}
|
||||
|
||||
// DownloadChart handles GET /:repo/charts/:filename
|
||||
func (cra *ChartRepositoryAPI) DownloadChart() {
|
||||
// Check access
|
||||
if !cra.requireAccess(rbac.ActionRead) {
|
||||
return
|
||||
}
|
||||
|
||||
namespace := cra.GetStringFromPath(namespaceParam)
|
||||
fileName := cra.GetStringFromPath(filenameParam)
|
||||
// Add hook event to request context
|
||||
cra.addDownloadChartEventContext(fileName, namespace, cra.Ctx.Request)
|
||||
|
||||
// Directly proxy to the backend
|
||||
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
||||
}
|
||||
|
||||
// ListCharts handles GET /api/:repo/charts
|
||||
func (cra *ChartRepositoryAPI) ListCharts() {
|
||||
// Check access
|
||||
if !cra.requireAccess(rbac.ActionList) {
|
||||
return
|
||||
}
|
||||
|
||||
charts, err := chartController.ListCharts(cra.namespace)
|
||||
if err != nil {
|
||||
cra.ParseAndHandleError("fail to list charts", err)
|
||||
return
|
||||
}
|
||||
|
||||
cra.WriteJSONData(charts)
|
||||
}
|
||||
|
||||
// ListChartVersions GET /api/:repo/charts/:name
|
||||
func (cra *ChartRepositoryAPI) ListChartVersions() {
|
||||
// Check access
|
||||
if !cra.requireAccess(rbac.ActionList, rbac.ResourceHelmChartVersion) {
|
||||
return
|
||||
}
|
||||
|
||||
chartName := cra.GetStringFromPath(nameParam)
|
||||
|
||||
versions, err := chartController.GetChart(cra.namespace, chartName)
|
||||
if err != nil {
|
||||
cra.ParseAndHandleError("fail to get chart", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Append labels
|
||||
for _, chartVersion := range versions {
|
||||
labels, err := cra.labelManager.GetLabelsOfResource(common.ResourceTypeChart, chartFullName(cra.namespace, chartVersion.Name, chartVersion.Version))
|
||||
if err != nil {
|
||||
cra.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
chartVersion.Labels = labels
|
||||
}
|
||||
|
||||
cra.WriteJSONData(versions)
|
||||
}
|
||||
|
||||
// GetChartVersion handles GET /api/:repo/charts/:name/:version
|
||||
func (cra *ChartRepositoryAPI) GetChartVersion() {
|
||||
// Check access
|
||||
if !cra.requireAccess(rbac.ActionRead, rbac.ResourceHelmChartVersion) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get other parameters
|
||||
chartName := cra.GetStringFromPath(nameParam)
|
||||
version := cra.GetStringFromPath(versionParam)
|
||||
|
||||
chartVersion, err := chartController.GetChartVersionDetails(cra.namespace, chartName, version)
|
||||
if err != nil {
|
||||
cra.ParseAndHandleError("fail to get chart version", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Append labels
|
||||
labels, err := cra.labelManager.GetLabelsOfResource(common.ResourceTypeChart, chartFullName(cra.namespace, chartName, version))
|
||||
if err != nil {
|
||||
cra.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
chartVersion.Labels = labels
|
||||
|
||||
cra.WriteJSONData(chartVersion)
|
||||
}
|
||||
|
||||
// DeleteChartVersion handles DELETE /api/:repo/charts/:name/:version
|
||||
func (cra *ChartRepositoryAPI) DeleteChartVersion() {
|
||||
// Check access
|
||||
if !cra.requireAccess(rbac.ActionDelete, rbac.ResourceHelmChartVersion) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get other parameters
|
||||
chartName := cra.GetStringFromPath(nameParam)
|
||||
version := cra.GetStringFromPath(versionParam)
|
||||
|
||||
// Try to remove labels from deleting chart if existing
|
||||
if err := cra.removeLabelsFromChart(chartName, version); err != nil {
|
||||
cra.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := chartController.DeleteChartVersion(cra.namespace, chartName, version); err != nil {
|
||||
cra.ParseAndHandleError("fail to delete chart version", err)
|
||||
return
|
||||
}
|
||||
|
||||
event := &n_event.Event{}
|
||||
metaData := &metadata.ChartDeleteMetaData{
|
||||
ChartMetaData: metadata.ChartMetaData{
|
||||
ProjectName: cra.namespace,
|
||||
ChartName: chartName,
|
||||
Versions: []string{version},
|
||||
OccurAt: time.Now(),
|
||||
Operator: cra.SecurityCtx.GetUsername(),
|
||||
},
|
||||
}
|
||||
if err := event.Build(metaData); err == nil {
|
||||
if err := event.Publish(); err != nil {
|
||||
hlog.Errorf("failed to publish chart delete event: %v", err)
|
||||
}
|
||||
} else {
|
||||
hlog.Errorf("failed to build chart delete event metadata: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// UploadChartVersion handles POST /api/:repo/charts
|
||||
func (cra *ChartRepositoryAPI) UploadChartVersion() {
|
||||
hlog.Debugf("Header of request of uploading chart: %#v, content-len=%d", cra.Ctx.Request.Header, cra.Ctx.Request.ContentLength)
|
||||
|
||||
// Check access
|
||||
if !cra.requireAccess(rbac.ActionCreate, rbac.ResourceHelmChartVersion) {
|
||||
return
|
||||
}
|
||||
|
||||
// Rewrite file content if the content type is "multipart/form-data"
|
||||
if isMultipartFormData(cra.Ctx.Request) {
|
||||
formFiles := make([]formFile, 0)
|
||||
formFiles = append(formFiles,
|
||||
formFile{
|
||||
formField: formFieldNameForChart,
|
||||
mustHave: true,
|
||||
},
|
||||
formFile{
|
||||
formField: formFiledNameForProv,
|
||||
})
|
||||
if err := cra.rewriteFileContent(formFiles, cra.Ctx.Request); err != nil {
|
||||
cra.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
if err := cra.addEventContext(formFiles, cra.Ctx.Request); err != nil {
|
||||
hlog.Errorf("Failed to add chart upload context, %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Directly proxy to the backend
|
||||
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
||||
}
|
||||
|
||||
// UploadChartProvFile handles POST /api/:repo/prov
|
||||
func (cra *ChartRepositoryAPI) UploadChartProvFile() {
|
||||
// Check access
|
||||
if !cra.requireAccess(rbac.ActionCreate) {
|
||||
return
|
||||
}
|
||||
|
||||
// Rewrite file content if the content type is "multipart/form-data"
|
||||
if isMultipartFormData(cra.Ctx.Request) {
|
||||
formFiles := make([]formFile, 0)
|
||||
formFiles = append(formFiles,
|
||||
formFile{
|
||||
formField: formFiledNameForProv,
|
||||
mustHave: true,
|
||||
})
|
||||
if err := cra.rewriteFileContent(formFiles, cra.Ctx.Request); err != nil {
|
||||
cra.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Directly proxy to the backend
|
||||
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
||||
}
|
||||
|
||||
// DeleteChart deletes all the chart versions of the specified chart.
|
||||
func (cra *ChartRepositoryAPI) DeleteChart() {
|
||||
// Check access
|
||||
if !cra.requireAccess(rbac.ActionDelete) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get other parameters from the request
|
||||
chartName := cra.GetStringFromPath(nameParam)
|
||||
|
||||
// Remove labels from all the deleting chart versions under the chart
|
||||
chartVersions, err := chartController.GetChart(cra.namespace, chartName)
|
||||
if err != nil {
|
||||
cra.ParseAndHandleError("fail to get chart", err)
|
||||
return
|
||||
}
|
||||
|
||||
versions := []string{}
|
||||
for _, chartVersion := range chartVersions {
|
||||
versions = append(versions, chartVersion.Version)
|
||||
if err := cra.removeLabelsFromChart(chartName, chartVersion.Version); err != nil {
|
||||
cra.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := chartController.DeleteChart(cra.namespace, chartName); err != nil {
|
||||
cra.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
event := &n_event.Event{}
|
||||
metaData := &metadata.ChartDeleteMetaData{
|
||||
ChartMetaData: metadata.ChartMetaData{
|
||||
ProjectName: cra.namespace,
|
||||
ChartName: chartName,
|
||||
Versions: versions,
|
||||
OccurAt: time.Now(),
|
||||
Operator: cra.SecurityCtx.GetUsername(),
|
||||
},
|
||||
}
|
||||
if err := event.Build(metaData); err == nil {
|
||||
if err := event.Publish(); err != nil {
|
||||
hlog.Errorf("failed to publish chart delete event: %v", err)
|
||||
}
|
||||
} else {
|
||||
hlog.Errorf("failed to build chart delete event metadata: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (cra *ChartRepositoryAPI) removeLabelsFromChart(chartName, version string) error {
|
||||
// Try to remove labels from deleting chart if existing
|
||||
resourceID := chartFullName(cra.namespace, chartName, version)
|
||||
labels, err := cra.labelManager.GetLabelsOfResource(common.ResourceTypeChart, resourceID)
|
||||
if err == nil && len(labels) > 0 {
|
||||
for _, l := range labels {
|
||||
if err := cra.labelManager.RemoveLabelFromResource(common.ResourceTypeChart, resourceID, l.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if there exists a valid namespace
|
||||
// Return true if it does
|
||||
// Return false if it does not
|
||||
func (cra *ChartRepositoryAPI) requireNamespace(namespace string) bool {
|
||||
// Actually, never should be like this
|
||||
if len(namespace) == 0 {
|
||||
cra.SendBadRequestError(errors.New(":repo should be in the request URL"))
|
||||
return false
|
||||
}
|
||||
|
||||
existing, err := cra.ProjectCtl.Exists(cra.Context(), namespace)
|
||||
if err != nil {
|
||||
// Check failed with error
|
||||
cra.SendInternalServerError(fmt.Errorf("failed to check existence of namespace %s with error: %s", namespace, err.Error()))
|
||||
return false
|
||||
}
|
||||
|
||||
// Not existing
|
||||
if !existing {
|
||||
cra.handleProjectNotFound(namespace)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// formFile is used to represent the uploaded files in the form
|
||||
type formFile struct {
|
||||
// form field key contains the form file
|
||||
formField string
|
||||
|
||||
// flag to indicate if the file identified by the 'formField'
|
||||
// must exist
|
||||
mustHave bool
|
||||
}
|
||||
|
||||
// The func is for event based chart replication policy.
|
||||
// It will add a context for uploading request with key chart_upload, and consumed by upload response.
|
||||
func (cra *ChartRepositoryAPI) addEventContext(files []formFile, request *http.Request) error {
|
||||
if len(files) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if f.formField == formFieldNameForChart {
|
||||
mFile, _, err := cra.GetFile(f.formField)
|
||||
if err != nil {
|
||||
hlog.Errorf("failed to read file content for upload event, %v", err)
|
||||
return err
|
||||
}
|
||||
var Buf bytes.Buffer
|
||||
_, err = io.Copy(&Buf, mFile)
|
||||
if err != nil {
|
||||
hlog.Errorf("failed to copy file content for upload event, %v", err)
|
||||
return err
|
||||
}
|
||||
chartOpr := chartserver.ChartOperator{}
|
||||
chartDetails, err := chartOpr.GetChartData(Buf.Bytes())
|
||||
if err != nil {
|
||||
hlog.Errorf("failed to get chart content for upload event, %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
extInfo := make(map[string]interface{})
|
||||
extInfo["operator"] = cra.SecurityCtx.GetUsername()
|
||||
extInfo["projectName"] = cra.namespace
|
||||
extInfo["chartName"] = chartDetails.Metadata.Name
|
||||
|
||||
var public bool
|
||||
|
||||
project, err := cra.ProjectCtl.Get(cra.Context(), cra.namespace)
|
||||
if err != nil {
|
||||
hlog.Errorf("failed to check the public of project %s: %v", cra.namespace, err)
|
||||
public = false
|
||||
} else {
|
||||
public = project.IsPublic()
|
||||
}
|
||||
|
||||
e := &rep_event.Event{
|
||||
Type: rep_event.EventTypeChartUpload,
|
||||
Resource: &model.Resource{
|
||||
Type: model.ResourceTypeChart,
|
||||
Metadata: &model.ResourceMetadata{
|
||||
Repository: &model.Repository{
|
||||
Name: fmt.Sprintf("%s/%s", cra.namespace, chartDetails.Metadata.Name),
|
||||
Metadata: map[string]interface{}{
|
||||
"public": strconv.FormatBool(public),
|
||||
},
|
||||
},
|
||||
Artifacts: []*model.Artifact{
|
||||
{
|
||||
Tags: []string{chartDetails.Metadata.Version},
|
||||
},
|
||||
},
|
||||
},
|
||||
ExtendedInfo: extInfo,
|
||||
},
|
||||
}
|
||||
*request = *(request.WithContext(context.WithValue(request.Context(), common.ChartUploadCtxKey, e)))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cra *ChartRepositoryAPI) addDownloadChartEventContext(fileName, namespace string, request *http.Request) {
|
||||
chartName, version := parseChartVersionFromFilename(fileName)
|
||||
event := &metadata.ChartDownloadMetaData{
|
||||
ChartMetaData: metadata.ChartMetaData{
|
||||
ProjectName: namespace,
|
||||
ChartName: chartName,
|
||||
Versions: []string{version},
|
||||
OccurAt: time.Now(),
|
||||
Operator: cra.SecurityCtx.GetUsername(),
|
||||
},
|
||||
}
|
||||
*request = *(request.WithContext(context.WithValue(request.Context(), common.ChartDownloadCtxKey, event)))
|
||||
}
|
||||
|
||||
// If the files are uploaded with multipart/form-data mimetype, beego will extract the data
|
||||
// from the request automatically. Then the request passed to the backend server with proxying
|
||||
// way will have empty content.
|
||||
// This method will refill the requests with file content.
|
||||
func (cra *ChartRepositoryAPI) rewriteFileContent(files []formFile, request *http.Request) error {
|
||||
if len(files) == 0 {
|
||||
return nil // no files, early return
|
||||
}
|
||||
|
||||
var body bytes.Buffer
|
||||
w := multipart.NewWriter(&body)
|
||||
defer func() {
|
||||
if err := w.Close(); err != nil {
|
||||
// Just log it
|
||||
hlog.Errorf("Failed to defer close multipart writer with error: %s", err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
// Process files by key one by one
|
||||
for _, f := range files {
|
||||
mFile, mHeader, err := cra.GetFile(f.formField)
|
||||
|
||||
// Handle error case by case
|
||||
if err != nil {
|
||||
formatedErr := fmt.Errorf("get file content with multipart header from key '%s' failed with error: %s", f.formField, err.Error())
|
||||
if f.mustHave || err != http.ErrMissingFile {
|
||||
return formatedErr
|
||||
}
|
||||
|
||||
// Error can be ignored, just log it
|
||||
hlog.Warning(formatedErr.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
fw, err := w.CreateFormFile(f.formField, mHeader.Filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create form file with multipart header failed with error: %s", err.Error())
|
||||
}
|
||||
|
||||
_, err = io.Copy(fw, mFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copy file stream in multipart form data failed with error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
request.Header.Set(headerContentType, w.FormDataContentType())
|
||||
request.ContentLength = -1
|
||||
request.Body = io.NopCloser(&body)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize the chart service controller
|
||||
func initializeChartController() (*chartserver.Controller, error) {
|
||||
addr, err := config.GetChartMuseumEndpoint()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get the endpoint URL of chart storage server: %s", err.Error())
|
||||
}
|
||||
|
||||
addr = strings.TrimSuffix(addr, "/")
|
||||
url, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
return nil, errors.New("endpoint URL of chart storage server is malformed")
|
||||
}
|
||||
|
||||
controller, err := chartserver.NewController(url, orm.Middleware())
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to initialize chart API controller")
|
||||
}
|
||||
|
||||
hlog.Debugf("Chart storage server is set to %s", url.String())
|
||||
hlog.Info("API controller for chart repository server is successfully initialized")
|
||||
|
||||
return controller, nil
|
||||
}
|
||||
|
||||
// Check if the request content type is "multipart/form-data"
|
||||
func isMultipartFormData(req *http.Request) bool {
|
||||
return strings.Contains(req.Header.Get(headerContentType), contentTypeMultipart)
|
||||
}
|
||||
|
||||
// Return the chart full name
|
||||
func chartFullName(namespace, chartName, version string) string {
|
||||
if strings.HasPrefix(chartName, "http") {
|
||||
return fmt.Sprintf("%s:%s", chartName, version)
|
||||
}
|
||||
return fmt.Sprintf("%s/%s:%s", namespace, chartName, version)
|
||||
}
|
||||
|
||||
// parseChartVersionFromFilename parse chart and version from file name
|
||||
func parseChartVersionFromFilename(filename string) (string, string) {
|
||||
noExt := strings.TrimSuffix(path.Base(filename), fmt.Sprintf(".%s", chartPackageFileExtension))
|
||||
parts := strings.Split(noExt, "-")
|
||||
lastIndex := len(parts) - 1
|
||||
name := parts[0]
|
||||
version := ""
|
||||
|
||||
for idx := lastIndex; idx >= 1; idx-- {
|
||||
if _, err := strconv.Atoi(string(parts[idx][0])); err == nil { // see if this part looks like a version (starts w int)
|
||||
version = strings.Join(parts[idx:], "-")
|
||||
name = strings.Join(parts[:idx], "-")
|
||||
break
|
||||
}
|
||||
}
|
||||
if version == "" { // no parts looked like a real version, just take everything after last hyphen
|
||||
name = strings.Join(parts[:lastIndex], "-")
|
||||
version = parts[lastIndex]
|
||||
}
|
||||
return name, version
|
||||
}
|
@ -1,234 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
bcontext "github.com/beego/beego/v2/server/web/context"
|
||||
|
||||
"github.com/goharbor/harbor/src/chartserver"
|
||||
proModels "github.com/goharbor/harbor/src/pkg/project/models"
|
||||
projecttesting "github.com/goharbor/harbor/src/testing/controller/project"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
)
|
||||
|
||||
var (
|
||||
crOldController *chartserver.Controller
|
||||
crMockServer *httptest.Server
|
||||
)
|
||||
|
||||
func TestIsMultipartFormData(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/chartrepo/charts", nil)
|
||||
|
||||
req.Header.Set(headerContentType, "application/json")
|
||||
if isMultipartFormData(req) {
|
||||
t.Fatal("expect false result but got true")
|
||||
}
|
||||
|
||||
req.Header.Set(headerContentType, contentTypeMultipart)
|
||||
if !isMultipartFormData(req) {
|
||||
t.Fatalf("expect %s result but got %s", contentTypeMultipart, req.Header.Get(headerContentType))
|
||||
}
|
||||
}
|
||||
|
||||
// Test namespace cheking
|
||||
func TestRequireNamespace(t *testing.T) {
|
||||
chartAPI := &ChartRepositoryAPI{}
|
||||
chartAPI.Ctx = bcontext.NewContext()
|
||||
chartAPI.Ctx.Request = httptest.NewRequest("GET", "/", nil)
|
||||
|
||||
projectCtl := &projecttesting.Controller{}
|
||||
chartAPI.ProjectCtl = projectCtl
|
||||
|
||||
mock.OnAnything(projectCtl, "List").Return([]*proModels.Project{
|
||||
{ProjectID: 0, Name: "library"},
|
||||
{ProjectID: 1, Name: "repo2"},
|
||||
}, nil)
|
||||
|
||||
mock.OnAnything(projectCtl, "Exists").Return(
|
||||
func(ctx context.Context, projectIDOrName interface{}) bool {
|
||||
if projectIDOrName == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if ns, ok := projectIDOrName.(string); ok {
|
||||
if ns == "library" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
func(ctx context.Context, projectIDOrName interface{}) error {
|
||||
if projectIDOrName == nil {
|
||||
return errors.New("nil projectIDOrName")
|
||||
}
|
||||
|
||||
if _, ok := projectIDOrName.(string); ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("unknown type of projectIDOrName")
|
||||
},
|
||||
)
|
||||
|
||||
if !chartAPI.requireNamespace("library") {
|
||||
t.Fatal("expect namespace 'library' existing but got false")
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare
|
||||
func TestPrepareEnv(t *testing.T) {
|
||||
var err error
|
||||
crMockServer, crOldController, err = mockChartController()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start mock chart service with error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test get health
|
||||
func TestGetHealthStatus(t *testing.T) {
|
||||
status := make(map[string]interface{})
|
||||
err := handleAndParse(&testingRequest{
|
||||
url: "/api/chartrepo/health",
|
||||
method: http.MethodGet,
|
||||
credential: sysAdmin,
|
||||
}, &status)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, ok := status["health"]; !ok {
|
||||
t.Fatal("expect 'health' but got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// Test get index by repo
|
||||
func TestGetIndexByRepo(t *testing.T) {
|
||||
runCodeCheckingCases(t, &codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: "/chartrepo/library/index.yaml",
|
||||
method: http.MethodGet,
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// Test get index
|
||||
func TestGetIndex(t *testing.T) {
|
||||
runCodeCheckingCases(t, &codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: "/chartrepo/index.yaml",
|
||||
method: http.MethodGet,
|
||||
credential: sysAdmin,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// Test download chart
|
||||
func TestDownloadChart(t *testing.T) {
|
||||
runCodeCheckingCases(t, &codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: "/chartrepo/library/charts/harbor-0.2.0.tgz",
|
||||
method: http.MethodGet,
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// Test get charts
|
||||
func TesListCharts(t *testing.T) {
|
||||
charts := make([]*chartserver.ChartInfo, 0)
|
||||
err := handleAndParse(&testingRequest{
|
||||
url: "/api/chartrepo/library/charts",
|
||||
method: http.MethodGet,
|
||||
credential: projAdmin,
|
||||
}, &charts)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(charts) != 2 {
|
||||
t.Fatalf("expect 2 charts but got %d", len(charts))
|
||||
}
|
||||
}
|
||||
|
||||
// Test get chart versions
|
||||
func TestListChartVersions(t *testing.T) {
|
||||
chartVersions := make(chartserver.ChartVersions, 0)
|
||||
err := handleAndParse(&testingRequest{
|
||||
url: "/api/chartrepo/library/charts/harbor",
|
||||
method: http.MethodGet,
|
||||
credential: projAdmin,
|
||||
}, &chartVersions)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(chartVersions) != 2 {
|
||||
t.Fatalf("expect 2 chart versions but got %d", len(chartVersions))
|
||||
}
|
||||
}
|
||||
|
||||
// Test get chart version details
|
||||
func TestGetChartVersion(t *testing.T) {
|
||||
chartV := &chartserver.ChartVersionDetails{}
|
||||
err := handleAndParse(&testingRequest{
|
||||
url: "/api/chartrepo/library/charts/harbor/0.2.0",
|
||||
method: http.MethodGet,
|
||||
credential: projAdmin,
|
||||
}, chartV)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if chartV.Metadata.Name != "harbor" {
|
||||
t.Fatalf("expect get chart 'harbor' but got %s", chartV.Metadata.Name)
|
||||
}
|
||||
|
||||
if chartV.Metadata.Version != "0.2.0" {
|
||||
t.Fatalf("expect get chart version '0.2.0' but got %s", chartV.Metadata.Version)
|
||||
}
|
||||
}
|
||||
|
||||
// Test delete chart version
|
||||
func TestDeleteChartVersion(t *testing.T) {
|
||||
runCodeCheckingCases(t, &codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: "/api/chartrepo/library/charts/harbor/0.2.1",
|
||||
method: http.MethodDelete,
|
||||
credential: projAdmin,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// Test delete chart
|
||||
func TestDeleteChart(t *testing.T) {
|
||||
runCodeCheckingCases(t, &codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: "/api/chartrepo/library/charts/harbor",
|
||||
method: http.MethodDelete,
|
||||
credential: projAdmin,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// Clear
|
||||
func TestClearEnv(t *testing.T) {
|
||||
crMockServer.Close()
|
||||
chartController = crOldController
|
||||
}
|
@ -25,7 +25,6 @@ import (
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/dghubble/sling"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/api"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/job/test"
|
||||
testutils "github.com/goharbor/harbor/src/common/utils/test"
|
||||
@ -87,27 +86,6 @@ func init() {
|
||||
web.BConfig.WebConfig.Session.SessionOn = true
|
||||
web.TestBeegoInit(apppath)
|
||||
|
||||
// Charts are controlled under projects
|
||||
chartRepositoryAPIType := &ChartRepositoryAPI{}
|
||||
web.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus")
|
||||
web.Router("/api/chartrepo/:repo/charts", chartRepositoryAPIType, "get:ListCharts")
|
||||
web.Router("/api/chartrepo/:repo/charts/:name", chartRepositoryAPIType, "get:ListChartVersions")
|
||||
web.Router("/api/chartrepo/:repo/charts/:name", chartRepositoryAPIType, "delete:DeleteChart")
|
||||
web.Router("/api/chartrepo/:repo/charts/:name/:version", chartRepositoryAPIType, "get:GetChartVersion")
|
||||
web.Router("/api/chartrepo/:repo/charts/:name/:version", chartRepositoryAPIType, "delete:DeleteChartVersion")
|
||||
web.Router("/api/chartrepo/:repo/charts", chartRepositoryAPIType, "post:UploadChartVersion")
|
||||
web.Router("/api/chartrepo/:repo/prov", chartRepositoryAPIType, "post:UploadChartProvFile")
|
||||
web.Router("/api/chartrepo/charts", chartRepositoryAPIType, "post:UploadChartVersion")
|
||||
|
||||
// Repository services
|
||||
web.Router("/chartrepo/:repo/index.yaml", chartRepositoryAPIType, "get:GetIndexByRepo")
|
||||
web.Router("/chartrepo/index.yaml", chartRepositoryAPIType, "get:GetIndex")
|
||||
web.Router("/chartrepo/:repo/charts/:filename", chartRepositoryAPIType, "get:DownloadChart")
|
||||
// Labels for chart
|
||||
chartLabelAPIType := &ChartLabelAPI{}
|
||||
web.Router("/api/"+api.APIVersion+"/chartrepo/:repo/charts/:name/:version/labels", chartLabelAPIType, "get:GetLabels;post:MarkLabel")
|
||||
web.Router("/api/"+api.APIVersion+"/chartrepo/:repo/charts/:name/:version/labels/:id([0-9]+)", chartLabelAPIType, "delete:RemoveLabel")
|
||||
|
||||
web.Router("/api/internal/syncquota", &InternalAPI{}, "post:SyncQuota")
|
||||
|
||||
// Init user Info
|
||||
|
@ -20,10 +20,37 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/replication/transfer"
|
||||
// import chart transfer
|
||||
_ "github.com/goharbor/harbor/src/controller/replication/transfer/chart"
|
||||
// import image transfer
|
||||
_ "github.com/goharbor/harbor/src/controller/replication/transfer/image"
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
|
||||
// import aliacr adapter
|
||||
_ "github.com/goharbor/harbor/src/pkg/reg/adapter/aliacr"
|
||||
// import awsecr adapter
|
||||
_ "github.com/goharbor/harbor/src/pkg/reg/adapter/awsecr"
|
||||
// import azurecr adapter
|
||||
_ "github.com/goharbor/harbor/src/pkg/reg/adapter/azurecr"
|
||||
// import dockerhub adapter
|
||||
_ "github.com/goharbor/harbor/src/pkg/reg/adapter/dockerhub"
|
||||
// import dtr adapter
|
||||
_ "github.com/goharbor/harbor/src/pkg/reg/adapter/dtr"
|
||||
// import githubcr adapter
|
||||
_ "github.com/goharbor/harbor/src/pkg/reg/adapter/githubcr"
|
||||
// import gitlab adapter
|
||||
_ "github.com/goharbor/harbor/src/pkg/reg/adapter/gitlab"
|
||||
// import googlegcr adapter
|
||||
_ "github.com/goharbor/harbor/src/pkg/reg/adapter/googlegcr"
|
||||
// import harbor adapter
|
||||
_ "github.com/goharbor/harbor/src/pkg/reg/adapter/harbor"
|
||||
// import huawei adapter
|
||||
_ "github.com/goharbor/harbor/src/pkg/reg/adapter/huawei"
|
||||
// import jfrog adapter
|
||||
_ "github.com/goharbor/harbor/src/pkg/reg/adapter/jfrog"
|
||||
// import native adapter
|
||||
_ "github.com/goharbor/harbor/src/pkg/reg/adapter/native"
|
||||
// import quay adapter
|
||||
_ "github.com/goharbor/harbor/src/pkg/reg/adapter/quay"
|
||||
// import tencentcr adapter
|
||||
_ "github.com/goharbor/harbor/src/pkg/reg/adapter/tencentcr"
|
||||
"github.com/goharbor/harbor/src/pkg/reg/model"
|
||||
)
|
||||
|
||||
|
@ -49,21 +49,6 @@ func TestParseParam(t *testing.T) {
|
||||
assert.Equal(t, "tom", p.Name)
|
||||
}
|
||||
|
||||
func TestParseParams(t *testing.T) {
|
||||
params := map[string]interface{}{
|
||||
"src_resource": `{"type":"chart"}`,
|
||||
"dst_resource": `{"type":"chart"}`,
|
||||
"speed": 1024,
|
||||
"copy_by_chunk": true,
|
||||
}
|
||||
res, dst, opts, err := parseParams(params)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, "chart", string(res.Type))
|
||||
assert.Equal(t, "chart", string(dst.Type))
|
||||
assert.Equal(t, int32(1024), opts.Speed)
|
||||
assert.True(t, opts.CopyByChunk)
|
||||
}
|
||||
|
||||
func TestMaxFails(t *testing.T) {
|
||||
rep := &Replication{}
|
||||
assert.Equal(t, uint(3), rep.MaxFails())
|
||||
|
@ -24,12 +24,11 @@ func TestCfgMetaData_InitFromArray(t *testing.T) {
|
||||
testArray := []Item{
|
||||
{Scope: SystemScope, Group: BasicGroup, EnvKey: "HARBOR_ADMIN_PASSWORD", DefaultValue: "", Name: common.AdminInitialPassword, ItemType: &PasswordType{}, Editable: true},
|
||||
{Scope: UserScope, Group: BasicGroup, EnvKey: "AUTH_MODE", DefaultValue: "db_auth", Name: common.AUTHMode, ItemType: &StringType{}, Editable: false},
|
||||
{Scope: SystemScope, Group: BasicGroup, EnvKey: "CHART_REPOSITORY_URL", DefaultValue: "http://chartmuseum:9999", Name: common.ChartRepoURL, ItemType: &StringType{}, Editable: false},
|
||||
}
|
||||
curInst := Instance()
|
||||
curInst.initFromArray(testArray)
|
||||
|
||||
if len(metaDataInstance.metaMap) != 3 {
|
||||
if len(metaDataInstance.metaMap) != 2 {
|
||||
t.Errorf("Can not initial metadata, size %v", len(metaDataInstance.metaMap))
|
||||
}
|
||||
item, ok := curInst.GetByName(common.AdminInitialPassword)
|
||||
|
@ -65,8 +65,8 @@ var (
|
||||
|
||||
{Name: common.AdminInitialPassword, Scope: SystemScope, Group: BasicGroup, EnvKey: "HARBOR_ADMIN_PASSWORD", DefaultValue: "", ItemType: &PasswordType{}, Editable: true},
|
||||
{Name: common.AUTHMode, Scope: UserScope, Group: BasicGroup, EnvKey: "AUTH_MODE", DefaultValue: "db_auth", ItemType: &AuthModeType{}, Editable: false, Description: `The auth mode of current system, such as "db_auth", "ldap_auth", "oidc_auth"`},
|
||||
|
||||
{Name: common.PrimaryAuthMode, Scope: UserScope, Group: BasicGroup, EnvKey: "PRIMARY_AUTH_MODE", DefaultValue: "false", ItemType: &BoolType{}, Description: `Use current auth mode as a primary one`},
|
||||
{Name: common.ChartRepoURL, Scope: SystemScope, Group: BasicGroup, EnvKey: "CHART_REPOSITORY_URL", DefaultValue: "http://chartmuseum:9999", ItemType: &StringType{}, Editable: false},
|
||||
|
||||
{Name: common.TrivyAdapterURL, Scope: SystemScope, Group: TrivyGroup, EnvKey: "TRIVY_ADAPTER_URL", DefaultValue: "http://trivy-adapter:8080", ItemType: &StringType{}, Editable: false},
|
||||
|
||||
@ -144,7 +144,6 @@ var (
|
||||
{Name: common.OIDCAutoOnboard, Scope: UserScope, Group: OIDCGroup, DefaultValue: "false", ItemType: &BoolType{}, Description: `Auto onboard the OIDC user`},
|
||||
{Name: common.OIDCExtraRedirectParms, Scope: UserScope, Group: OIDCGroup, DefaultValue: "{}", ItemType: &StringToStringMapType{}, Description: `Extra parameters to add when redirect request to OIDC provider`},
|
||||
|
||||
{Name: common.WithChartMuseum, Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CHARTMUSEUM", DefaultValue: "false", ItemType: &BoolType{}, Editable: true},
|
||||
{Name: common.WithTrivy, Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_TRIVY", DefaultValue: "false", ItemType: &BoolType{}, Editable: true},
|
||||
{Name: common.WithNotary, Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_NOTARY", DefaultValue: "false", ItemType: &BoolType{}, Editable: true},
|
||||
// the unit of expiration is days
|
||||
|
@ -30,7 +30,6 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -152,21 +151,6 @@ func WithTrivy() bool {
|
||||
return DefaultMgr().Get(backgroundCtx, common.WithTrivy).GetBool()
|
||||
}
|
||||
|
||||
// WithChartMuseum returns a bool to indicate if chartmuseum is deployed with Harbor.
|
||||
func WithChartMuseum() bool {
|
||||
return DefaultMgr().Get(backgroundCtx, common.WithChartMuseum).GetBool()
|
||||
}
|
||||
|
||||
// GetChartMuseumEndpoint returns the endpoint of the chartmuseum service
|
||||
// otherwise an non nil error is returned
|
||||
func GetChartMuseumEndpoint() (string, error) {
|
||||
chartEndpoint := strings.TrimSpace(DefaultMgr().Get(backgroundCtx, common.ChartRepoURL).GetString())
|
||||
if len(chartEndpoint) == 0 {
|
||||
return "", errors.New("empty chartmuseum endpoint")
|
||||
}
|
||||
return chartEndpoint, nil
|
||||
}
|
||||
|
||||
// ExtEndpoint returns the external URL of Harbor: protocol://host:port
|
||||
func ExtEndpoint() (string, error) {
|
||||
return DefaultMgr().Get(backgroundCtx, common.ExtEndpoint).GetString(), nil
|
||||
|
@ -42,9 +42,8 @@ func TestConfig(t *testing.T) {
|
||||
dao.PrepareTestData([]string{"delete from properties where k='scan_all_policy'"}, []string{})
|
||||
defaultCACertPath = path.Join(currPath(), "test", "ca.crt")
|
||||
c := map[string]interface{}{
|
||||
common.WithTrivy: false,
|
||||
common.WithChartMuseum: false,
|
||||
common.WithNotary: false,
|
||||
common.WithTrivy: false,
|
||||
common.WithNotary: false,
|
||||
}
|
||||
Init()
|
||||
|
||||
|
@ -26,8 +26,6 @@ import (
|
||||
const (
|
||||
// Image kind
|
||||
Image = "image"
|
||||
// Chart kind
|
||||
Chart = "chart"
|
||||
)
|
||||
|
||||
// Repository of candidate
|
||||
@ -71,7 +69,7 @@ type Candidate struct {
|
||||
// Repository name
|
||||
Repository string `json:"repository"`
|
||||
// Kind of the candidate
|
||||
// "image" or "chart"
|
||||
// "image"
|
||||
Kind string `json:"kind"`
|
||||
// Tags attached with the candidate
|
||||
Tags []string `json:"tags"`
|
||||
|
@ -30,7 +30,7 @@ func init() {
|
||||
// Artifact model in database
|
||||
type Artifact struct {
|
||||
ID int64 `orm:"pk;auto;column(id)"`
|
||||
Type string `orm:"column(type)"` // image or chart
|
||||
Type string `orm:"column(type)"` // image, chart or other OCI compatible
|
||||
MediaType string `orm:"column(media_type)"` // the media type of artifact
|
||||
ManifestMediaType string `orm:"column(manifest_media_type)"` // the media type of manifest/index
|
||||
ProjectID int64 `orm:"column(project_id)"` // needed for quota
|
||||
|
@ -31,7 +31,7 @@ import (
|
||||
// for all users.
|
||||
type Artifact struct {
|
||||
ID int64 `json:"id"`
|
||||
Type string `json:"type"` // image, chart, etc
|
||||
Type string `json:"type"` // image, chart or other OCI compatible
|
||||
MediaType string `json:"media_type"` // the media type of artifact. Mostly, it's the value of `manifest.config.mediatype`
|
||||
ManifestMediaType string `json:"manifest_media_type"` // the media type of manifest/index
|
||||
ProjectID int64 `json:"project_id"`
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,28 +0,0 @@
|
||||
package chart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
)
|
||||
|
||||
func TestGetChartDetails(t *testing.T) {
|
||||
chartOpr := NewOperator()
|
||||
_, err := chartOpr.GetDetails(htesting.HelmChartContent)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// ToDo add a v3 supported test data
|
||||
// if len(chartDetails.Dependencies) == 0 {
|
||||
// t.Fatal("At least 1 dependency exitsing, but we got 0 now")
|
||||
// }
|
||||
|
||||
// if len(chartDetails.Values) == 0 {
|
||||
// t.Fatal("At least 1 value existing, but we got 0 now")
|
||||
// }
|
||||
|
||||
// if chartDetails.Values["adminserver.adminPassword"] != "Harbor12345" {
|
||||
// t.Fatalf("The value of 'adminserver.adminPassword' should be 'Harbor12345' but we got '%s' now", chartDetails.Values["adminserver.adminPassword"])
|
||||
// }
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor/src/chartserver"
|
||||
)
|
||||
|
||||
func (c *client) ListAllCharts(project, repository string) ([]*chartserver.ChartVersion, error) {
|
||||
url := c.buildURL(fmt.Sprintf("/api/chartrepo/%s/charts/%s", project, repository))
|
||||
var charts []*chartserver.ChartVersion
|
||||
if err := c.httpclient.Get(url, &charts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return charts, nil
|
||||
}
|
||||
|
||||
func (c *client) DeleteChart(project, repository, version string) error {
|
||||
url := c.buildURL(fmt.Sprintf("/api/chartrepo/%s/charts/%s/%s", project, repository, version))
|
||||
return c.httpclient.Delete(url)
|
||||
}
|
||||
|
||||
func (c *client) DeleteChartRepository(project, repository string) error {
|
||||
url := c.buildURL(fmt.Sprintf("/api/chartrepo/%s/charts/%s", project, repository))
|
||||
return c.httpclient.Delete(url)
|
||||
}
|
@ -18,7 +18,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/goharbor/harbor/src/chartserver"
|
||||
chttp "github.com/goharbor/harbor/src/common/http"
|
||||
"github.com/goharbor/harbor/src/common/http/modifier"
|
||||
modelsv2 "github.com/goharbor/harbor/src/controller/artifact"
|
||||
@ -29,7 +28,6 @@ import (
|
||||
// and we should expand it when needed
|
||||
type Client interface {
|
||||
ArtifactClient
|
||||
ChartClient
|
||||
}
|
||||
|
||||
// ArtifactClient defines the methods that an image client should implement
|
||||
@ -39,13 +37,6 @@ type ArtifactClient interface {
|
||||
DeleteArtifactRepository(project, repository string) error
|
||||
}
|
||||
|
||||
// ChartClient defines the methods that a chart client should implement
|
||||
type ChartClient interface {
|
||||
ListAllCharts(project, repository string) ([]*chartserver.ChartVersion, error)
|
||||
DeleteChart(project, repository, version string) error
|
||||
DeleteChartRepository(project, repository string) error
|
||||
}
|
||||
|
||||
// New returns an instance of the client which is a default implement for Client
|
||||
func New(url string, httpclient *http.Client, modifiers ...modifier.Modifier) Client {
|
||||
return &client{
|
||||
|
@ -99,7 +99,7 @@ func (suite *DaoTestSuite) TestGet() {
|
||||
|
||||
id, err := suite.dao.Create(orm.Context(), &model.Job{
|
||||
PolicyID: 2222,
|
||||
EventType: "pushChart",
|
||||
EventType: "pullImage",
|
||||
NotifyType: "http",
|
||||
Status: "pending",
|
||||
JobDetail: "{\"type\":\"pushImage\",\"occur_at\":1563536782,\"event_data\":{\"resources\":[{\"digest\":\"sha256:bf1684a6e3676389ec861c602e97f27b03f14178e5bc3f70dce198f9f160cce9\",\"tag\":\"v1.0\",\"resource_url\":\"10.194.32.23/myproj/alpine:v1.0\"}],\"repository\":{\"date_created\":1563505587,\"name\":\"alpine\",\"namespace\":\"myproj\",\"repo_full_name\":\"myproj/alpine\",\"repo_type\":\"private\"}},\"operator\":\"admin\"}",
|
||||
@ -109,7 +109,7 @@ func (suite *DaoTestSuite) TestGet() {
|
||||
|
||||
r, err := suite.dao.Get(orm.Context(), id)
|
||||
suite.Nil(err)
|
||||
suite.Equal("pushChart", r.EventType)
|
||||
suite.Equal("pullImage", r.EventType)
|
||||
}
|
||||
|
||||
func (suite *DaoTestSuite) TestUpdate() {
|
||||
@ -163,7 +163,7 @@ func (suite *DaoTestSuite) TestDeleteByPolicyID() {
|
||||
func (suite *DaoTestSuite) TestGetLastTriggerJobsGroupByEventType() {
|
||||
_, err := suite.dao.Create(orm.Context(), &model.Job{
|
||||
PolicyID: 3333,
|
||||
EventType: "pushChart",
|
||||
EventType: "replicateImage",
|
||||
NotifyType: "http",
|
||||
Status: "pending",
|
||||
JobDetail: "{\"type\":\"pushImage\",\"occur_at\":1563536782,\"event_data\":{\"resources\":[{\"digest\":\"sha256:bf1684a6e3676389ec861c602e97f27b03f14178e5bc3f70dce198f9f160cce9\",\"tag\":\"v1.0\",\"resource_url\":\"10.194.32.23/myproj/alpine:v1.0\"}],\"repository\":{\"date_created\":1563505587,\"name\":\"alpine\",\"namespace\":\"myproj\",\"repo_full_name\":\"myproj/alpine\",\"repo_type\":\"private\"}},\"operator\":\"admin\"}",
|
||||
@ -172,7 +172,7 @@ func (suite *DaoTestSuite) TestGetLastTriggerJobsGroupByEventType() {
|
||||
suite.Nil(err)
|
||||
_, err = suite.dao.Create(orm.Context(), &model.Job{
|
||||
PolicyID: 3333,
|
||||
EventType: "pullChart",
|
||||
EventType: "pullImage",
|
||||
NotifyType: "http",
|
||||
Status: "pending",
|
||||
JobDetail: "{\"type\":\"pushImage\",\"occur_at\":1563536782,\"event_data\":{\"resources\":[{\"digest\":\"sha256:bf1684a6e3676389ec861c602e97f27b03f14178e5bc3f70dce198f9f160cce9\",\"tag\":\"v1.0\",\"resource_url\":\"10.194.32.23/myproj/alpine:v1.0\"}],\"repository\":{\"date_created\":1563505587,\"name\":\"alpine\",\"namespace\":\"myproj\",\"repo_full_name\":\"myproj/alpine\",\"repo_type\":\"private\"}},\"operator\":\"admin\"}",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user