From d5b85a67489ffcaa34e20bcfa3ce3bd1db2cda16 Mon Sep 17 00:00:00 2001 From: Yan Date: Mon, 16 Jul 2018 16:50:28 +0800 Subject: [PATCH] Add the registry controller httpserver, it's responsible for controlling (#5265) docker regsitry. This version has the API to call regsitry GC with jobservice secret. Seprates it into a standalone container as do not want to invoke two processes in one container. It needs to mount the registry storage into this container in order to do GC, and needs to copy the registry binary into it. --- Makefile | 10 ++ make/common/templates/adminserver/env | 3 +- make/common/templates/registryctl/config.yml | 8 ++ make/common/templates/registryctl/env | 3 + make/dev/registryctl/Dockerfile | 12 ++ make/docker-compose.tpl | 23 ++- make/photon/Makefile | 11 +- make/photon/registry/Dockerfile | 2 +- make/photon/registryctl/Dockerfile | 25 ++++ make/photon/registryctl/start.sh | 20 +++ make/prepare | 15 +- src/common/const.go | 143 ++++++++++--------- src/common/registryctl/client.go | 46 ++++++ src/common/utils/test/registryctl.go | 62 ++++++++ src/registryctl/api/base.go | 44 ++++++ src/registryctl/api/base_test.go | 31 ++++ src/registryctl/api/health.go | 29 ++++ src/registryctl/api/health_test.go | 33 +++++ src/registryctl/api/registry.go | 58 ++++++++ src/registryctl/auth/auth.go | 32 +++++ src/registryctl/auth/secret.go | 63 ++++++++ src/registryctl/auth/secret_test.go | 51 +++++++ src/registryctl/client/client.go | 102 +++++++++++++ src/registryctl/client/client_test.go | 51 +++++++ src/registryctl/config/config.go | 103 +++++++++++++ src/registryctl/config/config_test.go | 66 +++++++++ src/registryctl/config_test.yml | 8 ++ src/registryctl/handlers/handler.go | 82 +++++++++++ src/registryctl/handlers/handler_test.go | 69 +++++++++ src/registryctl/handlers/router.go | 29 ++++ src/registryctl/main.go | 91 ++++++++++++ 31 files changed, 1247 insertions(+), 78 deletions(-) create mode 100644 make/common/templates/registryctl/config.yml create mode 100644 make/common/templates/registryctl/env create mode 100644 make/dev/registryctl/Dockerfile create mode 100644 make/photon/registryctl/Dockerfile create mode 100644 make/photon/registryctl/start.sh create mode 100644 src/common/registryctl/client.go create mode 100644 src/common/utils/test/registryctl.go create mode 100644 src/registryctl/api/base.go create mode 100644 src/registryctl/api/base_test.go create mode 100644 src/registryctl/api/health.go create mode 100644 src/registryctl/api/health_test.go create mode 100644 src/registryctl/api/registry.go create mode 100644 src/registryctl/auth/auth.go create mode 100644 src/registryctl/auth/secret.go create mode 100644 src/registryctl/auth/secret_test.go create mode 100644 src/registryctl/client/client.go create mode 100644 src/registryctl/client/client_test.go create mode 100644 src/registryctl/config/config.go create mode 100644 src/registryctl/config/config_test.go create mode 100644 src/registryctl/config_test.yml create mode 100644 src/registryctl/handlers/handler.go create mode 100644 src/registryctl/handlers/handler_test.go create mode 100644 src/registryctl/handlers/router.go create mode 100644 src/registryctl/main.go diff --git a/Makefile b/Makefile index 615feb41d..33a8640b0 100644 --- a/Makefile +++ b/Makefile @@ -136,10 +136,12 @@ GOIMAGEBUILD=$(GOIMAGEBUILDCMD) build GOBUILDPATH_ADMINSERVER=$(GOBUILDPATH)/src/adminserver GOBUILDPATH_UI=$(GOBUILDPATH)/src/ui GOBUILDPATH_JOBSERVICE=$(GOBUILDPATH)/src/jobservice +GOBUILDPATH_REGISTRYCTL=$(GOBUILDPATH)/src/registryctl GOBUILDMAKEPATH=$(GOBUILDPATH)/make GOBUILDMAKEPATH_ADMINSERVER=$(GOBUILDMAKEPATH)/dev/adminserver GOBUILDMAKEPATH_UI=$(GOBUILDMAKEPATH)/dev/ui GOBUILDMAKEPATH_JOBSERVICE=$(GOBUILDMAKEPATH)/dev/jobservice +GOBUILDMAKEPATH_REGISTRYCTL=$(GOBUILDMAKEPATH)/dev/registryctl GOLANGDOCKERFILENAME=Dockerfile.golang # binary @@ -149,6 +151,8 @@ UIBINARYPATH=$(MAKEDEVPATH)/ui UIBINARYNAME=harbor_ui JOBSERVICEBINARYPATH=$(MAKEDEVPATH)/jobservice JOBSERVICEBINARYNAME=harbor_jobservice +REGISTRYCTLBINARYPATH=$(MAKEDEVPATH)/registryctl +REGISTRYCTLBINARYNAME=harbor_registryctl # configfile CONFIGPATH=$(MAKEPATH) @@ -179,6 +183,7 @@ DOCKERIMAGENAME_JOBSERVICE=vmware/harbor-jobservice DOCKERIMAGENAME_LOG=vmware/harbor-log DOCKERIMAGENAME_DB=vmware/harbor-db DOCKERIMAGENAME_CLARITY=vmware/harbor-clarity-ui-builder +DOCKERIMAGENAME_REGCTL=vmware/harbor-registryctl # docker-compose files DOCKERCOMPOSEFILEPATH=$(MAKEPATH) @@ -209,6 +214,7 @@ DOCKERSAVE_PARA=$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) \ $(DOCKERIMAGENAME_LOG):$(VERSIONTAG) \ $(DOCKERIMAGENAME_DB):$(VERSIONTAG) \ $(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \ + $(DOCKERIMAGENAME_REGCTL):$(VERSIONTAG) \ vmware/redis-photon:$(REDISVERSION) \ vmware/nginx-photon:$(NGINXVERSION) vmware/registry-photon:$(REGISTRYVERSION)-$(VERSIONTAG) \ vmware/photon:$(PHOTONVERSION) @@ -273,6 +279,10 @@ compile_golangimage: compile_clarity @$(DOCKERCMD) run --rm -v $(BUILDPATH):$(GOBUILDPATH) -w $(GOBUILDPATH_JOBSERVICE) $(GOBUILDIMAGE) $(GOIMAGEBUILD) -o $(GOBUILDMAKEPATH_JOBSERVICE)/$(JOBSERVICEBINARYNAME) @echo "Done." + @echo "compiling binary for harbor regsitry controller (golang image)..." + @$(DOCKERCMD) run --rm -v $(BUILDPATH):$(GOBUILDPATH) -w $(GOBUILDPATH_REGISTRYCTL) $(GOBUILDIMAGE) $(GOIMAGEBUILD) -o $(GOBUILDMAKEPATH_REGISTRYCTL)/$(REGISTRYCTLBINARYNAME) + @echo "Done." + compile:check_environment compile_golangimage prepare: diff --git a/make/common/templates/adminserver/env b/make/common/templates/adminserver/env index 33226706d..9fbeafb31 100644 --- a/make/common/templates/adminserver/env +++ b/make/common/templates/adminserver/env @@ -61,4 +61,5 @@ REGISTRY_STORAGE_PROVIDER_NAME=$storage_provider_name READ_ONLY=false SKIP_RELOAD_ENV_PATTERN=$skip_reload_env_pattern RELOAD_KEY=$reload_key -LDAP_GROUP_ADMIN_DN=$ldap_group_admin_dn \ No newline at end of file +LDAP_GROUP_ADMIN_DN=$ldap_group_admin_dn +REGISTRY_CONTROLLER_URL=$registry_controller_url \ No newline at end of file diff --git a/make/common/templates/registryctl/config.yml b/make/common/templates/registryctl/config.yml new file mode 100644 index 000000000..7b2a1d910 --- /dev/null +++ b/make/common/templates/registryctl/config.yml @@ -0,0 +1,8 @@ +--- +protocol: "http" +port: 8080 +log_level: "INFO" + +#https_config: +# cert: "server.crt" +# key: "server.key" \ No newline at end of file diff --git a/make/common/templates/registryctl/env b/make/common/templates/registryctl/env new file mode 100644 index 000000000..188efbcf4 --- /dev/null +++ b/make/common/templates/registryctl/env @@ -0,0 +1,3 @@ +UI_SECRET=$ui_secret +JOBSERVICE_SECRET=$jobservice_secret + diff --git a/make/dev/registryctl/Dockerfile b/make/dev/registryctl/Dockerfile new file mode 100644 index 000000000..84fd27304 --- /dev/null +++ b/make/dev/registryctl/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:1.9.2 + +MAINTAINER wangyan@vmware.com + +COPY . /go/src/github.com/vmware/harbor + +WORKDIR /go/src/github.com/vmware/harbor/src/registryctl + +RUN go build -a -o /go/bin/harbor_registryctl \ + && chmod u+x /go/bin/harbor_registryctl +WORKDIR /go/bin/ +ENTRYPOINT ["/go/bin/harbor_registryctl"] diff --git a/make/docker-compose.tpl b/make/docker-compose.tpl index b78a62a4f..9f70d7413 100644 --- a/make/docker-compose.tpl +++ b/make/docker-compose.tpl @@ -22,8 +22,6 @@ services: - harbor environment: - GODEBUG=netdns=cgo - command: - ["serve", "/etc/registry/config.yml"] depends_on: - log logging: @@ -31,6 +29,27 @@ services: options: syslog-address: "tcp://127.0.0.1:1514" tag: "registry" + registryctl: + image: vmware/harbor-registryctl:__version__ + container_name: registryctl + env_file: + - ./common/config/registryctl/env + restart: always + volumes: + - /data/registry:/storage:z + - ./common/config/registry/:/etc/registry/:z + - ./common/config/registryctl/config.yml:/etc/registryctl/config.yml:z + networks: + - harbor + environment: + - GODEBUG=netdns=cgo + depends_on: + - log + logging: + driver: "syslog" + options: + syslog-address: "tcp://127.0.0.1:1514" + tag: "registryctl" postgresql: image: vmware/harbor-db:__version__ container_name: harbor-db diff --git a/make/photon/Makefile b/make/photon/Makefile index f85bb07fb..ec69b6d0a 100644 --- a/make/photon/Makefile +++ b/make/photon/Makefile @@ -71,6 +71,10 @@ DOCKERFILEPATH_REG=$(DOCKERFILEPATH)/registry DOCKERFILENAME_REG=Dockerfile DOCKERIMAGENAME_REG=vmware/registry-photon +DOCKERFILEPATH_REGISTRYCTL=$(DOCKERFILEPATH)/registryctl +DOCKERFILENAME_REGISTRYCTL=Dockerfile +DOCKERIMAGENAME_REGISTRYCTL=vmware/harbor-registryctl + DOCKERFILEPATH_NOTARY=$(DOCKERFILEPATH)/notary DOCKERFILENAME_NOTARYSIGNER=signer.Dockerfile DOCKERIMAGENAME_NOTARYSIGNER=vmware/notary-signer-photon @@ -156,6 +160,11 @@ _build_registry: fi @echo "building registry container for photon..." @cd $(DOCKERFILEPATH_REG) && chmod 655 $(DOCKERFILEPATH_REG)/binary/registry && $(DOCKERBUILD) -f $(DOCKERFILEPATH_REG)/$(DOCKERFILENAME_REG) -t $(DOCKERIMAGENAME_REG):$(REGISTRYVERSION)-$(VERSIONTAG) . + @echo "Done." + +_build_registryctl: + @echo "building registry controller for photon..." + $(DOCKERBUILD) -f $(DOCKERFILEPATH_REGISTRYCTL)/$(DOCKERFILENAME_REGISTRYCTL) -t $(DOCKERIMAGENAME_REGISTRYCTL):$(VERSIONTAG) . @rm -rf $(DOCKERFILEPATH_REG)/binary @echo "Done." @@ -173,7 +182,7 @@ define _get_binary $(WGET) --timeout 30 --no-check-certificate $1 -O $2 endef -build: _build_db _build_adminiserver _build_ui _build_jobservice _build_log _build_nginx _build_registry _build_notary _build_clair _build_redis _build_migrator +build: _build_db _build_adminiserver _build_ui _build_jobservice _build_log _build_nginx _build_registry _build_registryctl _build_notary _build_clair _build_redis _build_migrator cleanimage: @echo "cleaning image for photon..." diff --git a/make/photon/registry/Dockerfile b/make/photon/registry/Dockerfile index 75393eef4..84c284d28 100644 --- a/make/photon/registry/Dockerfile +++ b/make/photon/registry/Dockerfile @@ -22,4 +22,4 @@ HEALTHCHECK CMD curl 127.0.0.1:5000/ VOLUME ["/var/lib/registry"] EXPOSE 5000 ENTRYPOINT ["/entrypoint.sh"] -CMD ["/etc/registry/config.yml"] +CMD ["/etc/registry/config.yml"] \ No newline at end of file diff --git a/make/photon/registryctl/Dockerfile b/make/photon/registryctl/Dockerfile new file mode 100644 index 000000000..bb1ff01f6 --- /dev/null +++ b/make/photon/registryctl/Dockerfile @@ -0,0 +1,25 @@ +FROM vmware/photon:1.0 + +MAINTAINER wangyan@vmware.com + +RUN tdnf distro-sync -y || echo \ + && tdnf erase vim -y \ + && tdnf install sudo -y >> /dev/null\ + && tdnf clean all \ + && groupadd -r -g 10000 harbor && useradd --no-log-init -r -g 10000 -u 10000 harbor \ + && mkdir -p /etc/registry + +COPY ./make/photon/registry/binary/registry /usr/bin +COPY ./make/photon/registryctl/start.sh /harbor/ +COPY ./make/dev/registryctl/harbor_registryctl /harbor/ + +RUN chmod u+x /harbor/harbor_registryctl \ + && chmod u+x /usr/bin/registry \ + && chmod u+x /harbor/start.sh + +HEALTHCHECK CMD curl --fail -s http://127.0.0.1:8080/api/health || exit 1 + +VOLUME ["/var/lib/registry"] +WORKDIR /harbor/ + +ENTRYPOINT ["/harbor/start.sh"] diff --git a/make/photon/registryctl/start.sh b/make/photon/registryctl/start.sh new file mode 100644 index 000000000..49dedf8bd --- /dev/null +++ b/make/photon/registryctl/start.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +set -e + +# The directory /var/lib/registry is within the container, and used to store image in CI testing. +# So for now we need to chown to it to avoid failure in CI. +if [ -d /var/lib/registry ]; then + chown 10000:10000 -R /var/lib/registry +fi + +if [ -d /storage ]; then + if ! stat -c '%u:%g' /storage | grep -q '10000:10000' ; then + # 10000 is the id of harbor user/group. + # Usually NFS Server does not allow changing owner of the export directory, + # so need to skip this step and requires NFS Server admin to set its owner to 10000. + chown 10000:10000 -R /storage + fi +fi + +sudo -E -u \#10000 "/harbor/harbor_registryctl" "-c" "/etc/registryctl/config.yml" diff --git a/make/prepare b/make/prepare index d64888a9d..c1ce16a53 100755 --- a/make/prepare +++ b/make/prepare @@ -284,7 +284,7 @@ storage_provider_config = rcp.get("configuration", "registry_storage_provider_co # yaml requires 1 or more spaces between the key and value storage_provider_config = storage_provider_config.replace(":", ": ", 1) ui_secret = ''.join(random.choice(string.ascii_letters+string.digits) for i in range(16)) -jobservice_secret = ''.join(random.choice(string.ascii_letters+string.digits) for i in range(16)) +jobservice_secret = ''.join(random.choice(string.ascii_letters+string.digits) for i in range(16)) adminserver_config_dir = os.path.join(config_dir,"adminserver") if not os.path.exists(adminserver_config_dir): @@ -295,6 +295,7 @@ ui_certificates_dir = prep_conf_dir(ui_config_dir,"certificates") db_config_dir = prep_conf_dir(config_dir, "db") job_config_dir = prep_conf_dir(config_dir, "jobservice") registry_config_dir = prep_conf_dir(config_dir, "registry") +registryctl_config_dir = prep_conf_dir(config_dir, "registryctl") nginx_config_dir = prep_conf_dir (config_dir, "nginx") nginx_conf_d = prep_conf_dir(nginx_config_dir, "conf.d") log_config_dir = prep_conf_dir (config_dir, "log") @@ -305,6 +306,8 @@ ui_conf = os.path.join(config_dir, "ui", "app.conf") ui_cert_dir = os.path.join(config_dir, "ui", "certificates") jobservice_conf = os.path.join(config_dir, "jobservice", "config.yml") registry_conf = os.path.join(config_dir, "registry", "config.yml") +registryctl_conf_env = os.path.join(config_dir, "registryctl", "env") +registryctl_conf_yml = os.path.join(config_dir, "registryctl", "config.yml") db_conf_env = os.path.join(config_dir, "db", "env") job_conf_env = os.path.join(config_dir, "jobservice", "env") nginx_conf = os.path.join(config_dir, "nginx", "nginx.conf") @@ -312,6 +315,7 @@ cert_dir = os.path.join(config_dir, "nginx", "cert") log_rotate_config = os.path.join(config_dir, "log", "logrotate.conf") adminserver_url = "http://adminserver:8080" registry_url = "http://registry:5000" +registry_controller_url = "http://registryctl:8080" ui_url = "http://ui:8080" token_service_url = "http://ui:8080/service/token" @@ -404,7 +408,8 @@ render(os.path.join(templates_dir, "adminserver", "env"), clair_url=clair_url, notary_url=notary_url, reload_key=reload_key, - skip_reload_env_pattern=skip_reload_env_pattern + skip_reload_env_pattern=skip_reload_env_pattern, + registry_controller_url = registry_controller_url ) render(os.path.join(templates_dir, "ui", "env"), @@ -461,7 +466,13 @@ render(os.path.join(templates_dir, "log", "logrotate.conf"), log_rotate_count=log_rotate_count, log_rotate_size=log_rotate_size) +render(os.path.join(templates_dir, "registryctl", "env"), + registryctl_conf_env, + jobservice_secret=jobservice_secret, + ui_secret=ui_secret) + shutil.copyfile(os.path.join(templates_dir, "ui", "app.conf"), ui_conf) +shutil.copyfile(os.path.join(templates_dir, "registryctl", "config.yml"), registryctl_conf_yml) print("Generated configuration file: %s" % ui_conf) if auth_mode == "uaa_auth": diff --git a/src/common/const.go b/src/common/const.go index 5f8d15402..c15dcd39b 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -38,75 +38,76 @@ const ( ResourceTypeRepository = "r" ResourceTypeImage = "i" - ExtEndpoint = "ext_endpoint" - AUTHMode = "auth_mode" - DatabaseType = "database_type" - PostGreSQLHOST = "postgresql_host" - PostGreSQLPort = "postgresql_port" - PostGreSQLUsername = "postgresql_username" - PostGreSQLPassword = "postgresql_password" - PostGreSQLDatabase = "postgresql_database" - PostGreSQLSSLMode = "postgresql_sslmode" - SelfRegistration = "self_registration" - UIURL = "ui_url" - JobServiceURL = "jobservice_url" - LDAPURL = "ldap_url" - LDAPSearchDN = "ldap_search_dn" - LDAPSearchPwd = "ldap_search_password" - LDAPBaseDN = "ldap_base_dn" - LDAPUID = "ldap_uid" - LDAPFilter = "ldap_filter" - LDAPScope = "ldap_scope" - LDAPTimeout = "ldap_timeout" - LDAPVerifyCert = "ldap_verify_cert" - LDAPGroupBaseDN = "ldap_group_base_dn" - LDAPGroupSearchFilter = "ldap_group_search_filter" - LDAPGroupAttributeName = "ldap_group_attribute_name" - LDAPGroupSearchScope = "ldap_group_search_scope" - TokenServiceURL = "token_service_url" - RegistryURL = "registry_url" - EmailHost = "email_host" - EmailPort = "email_port" - EmailUsername = "email_username" - EmailPassword = "email_password" - EmailFrom = "email_from" - EmailSSL = "email_ssl" - EmailIdentity = "email_identity" - EmailInsecure = "email_insecure" - ProjectCreationRestriction = "project_creation_restriction" - MaxJobWorkers = "max_job_workers" - TokenExpiration = "token_expiration" - CfgExpiration = "cfg_expiration" - JobLogDir = "job_log_dir" - AdminInitialPassword = "admin_initial_password" - AdmiralEndpoint = "admiral_url" - WithNotary = "with_notary" - WithClair = "with_clair" - ScanAllPolicy = "scan_all_policy" - ClairDBPassword = "clair_db_password" - ClairDBHost = "clair_db_host" - ClairDBPort = "clair_db_port" - ClairDB = "clair_db" - ClairDBUsername = "clair_db_username" - UAAEndpoint = "uaa_endpoint" - UAAClientID = "uaa_client_id" - UAAClientSecret = "uaa_client_secret" - UAAVerifyCert = "uaa_verify_cert" - DefaultClairEndpoint = "http://clair:6060" - CfgDriverDB = "db" - CfgDriverJSON = "json" - NewHarborAdminName = "admin@harbor.local" - RegistryStorageProviderName = "registry_storage_provider_name" - UserMember = "u" - GroupMember = "g" - ReadOnly = "read_only" - ClairURL = "clair_url" - NotaryURL = "notary_url" - DefaultAdminserverEndpoint = "http://adminserver:8080" - DefaultJobserviceEndpoint = "http://jobservice:8080" - DefaultUIEndpoint = "http://ui:8080" - DefaultNotaryEndpoint = "http://notary-server:4443" - LdapGroupType = 1 - ReloadKey = "reload_key" - LdapGroupAdminDn = "ldap_group_admin_dn" + ExtEndpoint = "ext_endpoint" + AUTHMode = "auth_mode" + DatabaseType = "database_type" + PostGreSQLHOST = "postgresql_host" + PostGreSQLPort = "postgresql_port" + PostGreSQLUsername = "postgresql_username" + PostGreSQLPassword = "postgresql_password" + PostGreSQLDatabase = "postgresql_database" + PostGreSQLSSLMode = "postgresql_sslmode" + SelfRegistration = "self_registration" + UIURL = "ui_url" + JobServiceURL = "jobservice_url" + LDAPURL = "ldap_url" + LDAPSearchDN = "ldap_search_dn" + LDAPSearchPwd = "ldap_search_password" + LDAPBaseDN = "ldap_base_dn" + LDAPUID = "ldap_uid" + LDAPFilter = "ldap_filter" + LDAPScope = "ldap_scope" + LDAPTimeout = "ldap_timeout" + LDAPVerifyCert = "ldap_verify_cert" + LDAPGroupBaseDN = "ldap_group_base_dn" + LDAPGroupSearchFilter = "ldap_group_search_filter" + LDAPGroupAttributeName = "ldap_group_attribute_name" + LDAPGroupSearchScope = "ldap_group_search_scope" + TokenServiceURL = "token_service_url" + RegistryURL = "registry_url" + EmailHost = "email_host" + EmailPort = "email_port" + EmailUsername = "email_username" + EmailPassword = "email_password" + EmailFrom = "email_from" + EmailSSL = "email_ssl" + EmailIdentity = "email_identity" + EmailInsecure = "email_insecure" + ProjectCreationRestriction = "project_creation_restriction" + MaxJobWorkers = "max_job_workers" + TokenExpiration = "token_expiration" + CfgExpiration = "cfg_expiration" + JobLogDir = "job_log_dir" + AdminInitialPassword = "admin_initial_password" + AdmiralEndpoint = "admiral_url" + WithNotary = "with_notary" + WithClair = "with_clair" + ScanAllPolicy = "scan_all_policy" + ClairDBPassword = "clair_db_password" + ClairDBHost = "clair_db_host" + ClairDBPort = "clair_db_port" + ClairDB = "clair_db" + ClairDBUsername = "clair_db_username" + UAAEndpoint = "uaa_endpoint" + UAAClientID = "uaa_client_id" + UAAClientSecret = "uaa_client_secret" + UAAVerifyCert = "uaa_verify_cert" + DefaultClairEndpoint = "http://clair:6060" + CfgDriverDB = "db" + CfgDriverJSON = "json" + NewHarborAdminName = "admin@harbor.local" + RegistryStorageProviderName = "registry_storage_provider_name" + UserMember = "u" + GroupMember = "g" + ReadOnly = "read_only" + ClairURL = "clair_url" + NotaryURL = "notary_url" + DefaultAdminserverEndpoint = "http://adminserver:8080" + DefaultJobserviceEndpoint = "http://jobservice:8080" + DefaultUIEndpoint = "http://ui:8080" + DefaultNotaryEndpoint = "http://notary-server:4443" + LdapGroupType = 1 + ReloadKey = "reload_key" + LdapGroupAdminDn = "ldap_group_admin_dn" + DefaultRegistryControllerEndpoint = "http://registryctl:8080" ) diff --git a/src/common/registryctl/client.go b/src/common/registryctl/client.go new file mode 100644 index 000000000..fed8c055a --- /dev/null +++ b/src/common/registryctl/client.go @@ -0,0 +1,46 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registryctl + +import ( + "os" + + "github.com/vmware/harbor/src/common" + "github.com/vmware/harbor/src/common/utils/log" + "github.com/vmware/harbor/src/registryctl/client" +) + +var ( + // RegistryCtlClient is a client for registry controller + RegistryCtlClient client.Client +) + +// Init ... +func Init() { + initRegistryCtlClient() +} + +func initRegistryCtlClient() { + registryCtlURL := os.Getenv("REGISTRY_CONTROLLER_URL") + if len(registryCtlURL) == 0 { + registryCtlURL = common.DefaultRegistryControllerEndpoint + } + + log.Infof("initializing client for reigstry %s ...", registryCtlURL) + cfg := &client.Config{ + Secret: os.Getenv("JOBSERVICE_SECRET"), + } + RegistryCtlClient = client.NewClient(registryCtlURL, cfg) +} diff --git a/src/common/utils/test/registryctl.go b/src/common/utils/test/registryctl.go new file mode 100644 index 000000000..d621cea31 --- /dev/null +++ b/src/common/utils/test/registryctl.go @@ -0,0 +1,62 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "time" +) + +// GCResult ... +type GCResult struct { + Status bool `json:"status"` + Msg string `json:"msg"` + StartTime time.Time `json:"starttime"` + EndTime time.Time `json:"endtime"` +} + +// NewRegistryCtl returns a mock registry server +func NewRegistryCtl(config map[string]interface{}) (*httptest.Server, error) { + m := []*RequestHandlerMapping{} + + gcr := GCResult{true, "hello-world", time.Now(), time.Now()} + b, err := json.Marshal(gcr) + if err != nil { + return nil, err + } + + resp := &Response{ + StatusCode: http.StatusOK, + Body: b, + } + + m = append(m, &RequestHandlerMapping{ + Method: "GET", + Pattern: "/api/health", + Handler: Handler(&Response{ + StatusCode: http.StatusOK, + }), + }) + + m = append(m, &RequestHandlerMapping{ + Method: "POST", + Pattern: "/api/registry/gc", + Handler: Handler(resp), + }) + + return NewServer(m...), nil +} diff --git a/src/registryctl/api/base.go b/src/registryctl/api/base.go new file mode 100644 index 000000000..4b9002fd9 --- /dev/null +++ b/src/registryctl/api/base.go @@ -0,0 +1,44 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "encoding/json" + "net/http" +) + +func handleInternalServerError(w http.ResponseWriter) { + http.Error(w, http.StatusText(http.StatusInternalServerError), + http.StatusInternalServerError) +} + +func handleUnauthorized(w http.ResponseWriter) { + http.Error(w, http.StatusText(http.StatusUnauthorized), + http.StatusUnauthorized) +} + +// response status code will be written automatically if there is an error +func writeJSON(w http.ResponseWriter, v interface{}) error { + b, err := json.Marshal(v) + if err != nil { + handleInternalServerError(w) + return err + } + + if _, err = w.Write(b); err != nil { + return err + } + return nil +} diff --git a/src/registryctl/api/base_test.go b/src/registryctl/api/base_test.go new file mode 100644 index 000000000..50c58c2d3 --- /dev/null +++ b/src/registryctl/api/base_test.go @@ -0,0 +1,31 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestHandleInternalServerError(t *testing.T) { + w := httptest.NewRecorder() + handleInternalServerError(w) + + if w.Code != http.StatusInternalServerError { + t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusInternalServerError) + } + +} diff --git a/src/registryctl/api/health.go b/src/registryctl/api/health.go new file mode 100644 index 000000000..262757f3c --- /dev/null +++ b/src/registryctl/api/health.go @@ -0,0 +1,29 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + + "github.com/vmware/harbor/src/common/utils/log" +) + +// Health ... +func Health(w http.ResponseWriter, r *http.Request) { + if err := writeJSON(w, "healthy"); err != nil { + log.Errorf("Failed to write response: %v", err) + return + } +} diff --git a/src/registryctl/api/health_test.go b/src/registryctl/api/health_test.go new file mode 100644 index 000000000..e27f2f934 --- /dev/null +++ b/src/registryctl/api/health_test.go @@ -0,0 +1,33 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHealth(t *testing.T) { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "", nil) + Health(w, req) + assert.Equal(t, http.StatusOK, w.Code) + result, _ := ioutil.ReadAll(w.Body) + assert.Equal(t, "\"healthy\"", string(result)) +} diff --git a/src/registryctl/api/registry.go b/src/registryctl/api/registry.go new file mode 100644 index 000000000..4f7395c8d --- /dev/null +++ b/src/registryctl/api/registry.go @@ -0,0 +1,58 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "bytes" + "net/http" + "time" + + "os/exec" + + "github.com/vmware/harbor/src/common/utils/log" +) + +const ( + regConf = "/etc/registry/config.yml" +) + +// GCResult ... +type GCResult struct { + Status bool `json:"status"` + Msg string `json:"msg"` + StartTime time.Time `json:"starttime"` + EndTime time.Time `json:"endtime"` +} + +// StartGC ... +func StartGC(w http.ResponseWriter, r *http.Request) { + cmd := exec.Command("/bin/bash", "-c", "registry garbage-collect "+regConf) + var outBuf, errBuf bytes.Buffer + cmd.Stdout = &outBuf + cmd.Stderr = &errBuf + + start := time.Now() + if err := cmd.Run(); err != nil { + log.Errorf("Fail to execute GC: %v, command err: %s", err, errBuf.String()) + handleInternalServerError(w) + return + } + + gcr := GCResult{true, outBuf.String(), start, time.Now()} + if err := writeJSON(w, gcr); err != nil { + log.Errorf("failed to write response: %v", err) + return + } +} diff --git a/src/registryctl/auth/auth.go b/src/registryctl/auth/auth.go new file mode 100644 index 000000000..ce6d933dc --- /dev/null +++ b/src/registryctl/auth/auth.go @@ -0,0 +1,32 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +import ( + "errors" + "net/http" +) + +var ( + // ErrInvalidCredential is returned when the auth token does not authenticate correctly. + ErrInvalidCredential = errors.New("invalid authorization credential") +) + +// AuthenticationHandler is an interface for authorizing a request +type AuthenticationHandler interface { + + // AuthorizeRequest ... + AuthorizeRequest(req *http.Request) error +} diff --git a/src/registryctl/auth/secret.go b/src/registryctl/auth/secret.go new file mode 100644 index 000000000..da70527f8 --- /dev/null +++ b/src/registryctl/auth/secret.go @@ -0,0 +1,63 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +import ( + "errors" + "net/http" + "strings" + + "github.com/vmware/harbor/src/common/secret" +) + +//HarborSecret is the prefix of the value of Authorization header. +const HarborSecret = secret.HeaderPrefix + +var ( + // ErrNoSecret ... + ErrNoSecret = errors.New("no secret auth credentials") +) + +type secretHandler struct { + secrets map[string]string +} + +// NewSecretHandler creaters a new authentiation handler which adds +// basic authentication credentials to a request. +func NewSecretHandler(secrets map[string]string) AuthenticationHandler { + return &secretHandler{ + secrets: secrets, + } +} + +func (s *secretHandler) AuthorizeRequest(req *http.Request) error { + if len(s.secrets) == 0 || req == nil { + return ErrNoSecret + } + + auth := req.Header.Get("Authorization") + if !strings.HasPrefix(auth, HarborSecret) { + return ErrInvalidCredential + } + secInReq := strings.TrimPrefix(auth, HarborSecret) + + for _, v := range s.secrets { + if secInReq == v { + return nil + } + } + + return ErrInvalidCredential +} diff --git a/src/registryctl/auth/secret_test.go b/src/registryctl/auth/secret_test.go new file mode 100644 index 000000000..d76e514bf --- /dev/null +++ b/src/registryctl/auth/secret_test.go @@ -0,0 +1,51 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + commonsecret "github.com/vmware/harbor/src/common/secret" +) + +func TestAuthorizeRequestInvalid(t *testing.T) { + secret := "correct" + req, err := http.NewRequest("", "", nil) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + _ = commonsecret.AddToRequest(req, secret) + + authenticator := NewSecretHandler(map[string]string{"secret1": "incorrect"}) + err = authenticator.AuthorizeRequest(req) + assert.Equal(t, err, ErrInvalidCredential) + +} + +func TestAuthorizeRequestValid(t *testing.T) { + secret := "correct" + req, err := http.NewRequest("", "", nil) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + _ = commonsecret.AddToRequest(req, secret) + + authenticator := NewSecretHandler(map[string]string{"secret1": "correct"}) + err = authenticator.AuthorizeRequest(req) + assert.Nil(t, err) + +} diff --git a/src/registryctl/client/client.go b/src/registryctl/client/client.go new file mode 100644 index 000000000..c57b24747 --- /dev/null +++ b/src/registryctl/client/client.go @@ -0,0 +1,102 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package client + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + + common_http "github.com/vmware/harbor/src/common/http" + "github.com/vmware/harbor/src/common/http/modifier/auth" + "github.com/vmware/harbor/src/common/utils" + "github.com/vmware/harbor/src/common/utils/log" + "github.com/vmware/harbor/src/registryctl/api" +) + +// Client defines methods that an Regsitry client should implement +type Client interface { + // Health tests the connection with registry server + Health() error + // StartGC enable the gc of registry server + StartGC() (*api.GCResult, error) +} + +type client struct { + baseURL string + client *common_http.Client +} + +// Config contains configurations needed for client +type Config struct { + Secret string +} + +// NewClient return an instance of Registry client +func NewClient(baseURL string, cfg *Config) Client { + baseURL = strings.TrimRight(baseURL, "/") + if !strings.Contains(baseURL, "://") { + baseURL = "http://" + baseURL + } + client := &client{ + baseURL: baseURL, + } + if cfg != nil { + authorizer := auth.NewSecretAuthorizer(cfg.Secret) + client.client = common_http.NewClient(nil, authorizer) + } + return client +} + +// Health ... +func (c *client) Health() error { + addr := strings.Split(c.baseURL, "://")[1] + if !strings.Contains(addr, ":") { + addr = addr + ":80" + } + return utils.TestTCPConn(addr, 60, 2) +} + +// StartGC ... +func (c *client) StartGC() (*api.GCResult, error) { + url := c.baseURL + "/api/registry/gc" + gcr := &api.GCResult{} + + req, err := http.NewRequest(http.MethodPost, url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + log.Errorf("Failed to start gc: %d", resp.StatusCode) + return nil, fmt.Errorf("Failed to start GC: %d", resp.StatusCode) + } + if err := json.Unmarshal(data, gcr); err != nil { + return nil, err + } + + return gcr, nil +} diff --git a/src/registryctl/client/client_test.go b/src/registryctl/client/client_test.go new file mode 100644 index 000000000..e13447209 --- /dev/null +++ b/src/registryctl/client/client_test.go @@ -0,0 +1,51 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package client + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vmware/harbor/src/common/utils/test" +) + +var c Client + +func TestMain(m *testing.M) { + + server, err := test.NewRegistryCtl(nil) + if err != nil { + fmt.Printf("failed to create regsitry: %v", err) + os.Exit(1) + } + + c = NewClient(server.URL, &Config{}) + + os.Exit(m.Run()) +} + +func TesHealth(t *testing.T) { + err := c.Health() + assert.Nil(t, err) +} + +func TesStartGC(t *testing.T) { + gcr, err := c.StartGC() + assert.NotNil(t, err) + assert.Equal(t, gcr.Msg, "hello-world") + assert.Equal(t, gcr.Status, true) +} diff --git a/src/registryctl/config/config.go b/src/registryctl/config/config.go new file mode 100644 index 000000000..f7565fa17 --- /dev/null +++ b/src/registryctl/config/config.go @@ -0,0 +1,103 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "io/ioutil" + "os" + + yaml "gopkg.in/yaml.v2" +) + +//DefaultConfig ... +var DefaultConfig = &Configuration{} + +//Configuration loads the configuration of registry controller. +type Configuration struct { + Protocol string `yaml:"protocol"` + Port string `yaml:"port"` + LogLevel string `yaml:"log_level"` + HTTPSConfig struct { + Cert string `yaml:"cert"` + Key string `yaml:"key"` + } `yaml:"https_config,omitempty"` +} + +//Load the configuration options from the specified yaml file. +func (c *Configuration) Load(yamlFilePath string, detectEnv bool) error { + if len(yamlFilePath) != 0 { + //Try to load from file first + data, err := ioutil.ReadFile(yamlFilePath) + if err != nil { + return err + } + if err = yaml.Unmarshal(data, c); err != nil { + return err + } + } + + if detectEnv { + c.loadEnvs() + } + + return nil +} + +//GetLogLevel returns the log level +func GetLogLevel() string { + return DefaultConfig.LogLevel +} + +//GetJobAuthSecret get the auth secret from the env +func GetJobAuthSecret() string { + return os.Getenv("JOBSERVICE_SECRET") +} + +//GetUIAuthSecret get the auth secret of UI side +func GetUIAuthSecret() string { + return os.Getenv("UI_SECRET") +} + +//loadEnvs Load env variables +func (c *Configuration) loadEnvs() { + prot := os.Getenv("REGISTRYCTL_PROTOCOL") + if len(prot) != 0 { + c.Protocol = prot + } + + p := os.Getenv("PORT") + if len(p) != 0 { + c.Port = p + } + + //Only when protocol is https + if c.Protocol == "HTTPS" { + cert := os.Getenv("REGISTRYCTL_HTTPS_CERT") + if len(cert) != 0 { + c.HTTPSConfig.Cert = cert + } + + certKey := os.Getenv("REGISTRYCTL_HTTPS_KEY") + if len(certKey) != 0 { + c.HTTPSConfig.Key = certKey + } + } + + loggerLevel := os.Getenv("LOG_LEVEL") + if len(loggerLevel) != 0 { + c.LogLevel = loggerLevel + } + +} diff --git a/src/registryctl/config/config_test.go b/src/registryctl/config/config_test.go new file mode 100644 index 000000000..a4e713632 --- /dev/null +++ b/src/registryctl/config/config_test.go @@ -0,0 +1,66 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfigDoesNotExists(t *testing.T) { + cfg := &Configuration{} + err := cfg.Load("./config.not-existing.yaml", false) + assert.NotNil(t, err) +} + +func TestConfigLoadingWithEnv(t *testing.T) { + os.Setenv("REGISTRYCTL_PROTOCOL", "https") + os.Setenv("PORT", "1000") + os.Setenv("LOG_LEVEL", "DEBUG") + + cfg := &Configuration{} + err := cfg.Load("../config_test.yml", true) + assert.Nil(t, err) + assert.Equal(t, "https", cfg.Protocol) + assert.Equal(t, "1000", cfg.Port) + assert.Equal(t, "DEBUG", cfg.LogLevel) +} + +func TestConfigLoadingWithYml(t *testing.T) { + cfg := &Configuration{} + err := cfg.Load("../config_test.yml", false) + assert.Nil(t, err) + assert.Equal(t, "http", cfg.Protocol) + assert.Equal(t, "1234", cfg.Port) + assert.Equal(t, "ERROR", cfg.LogLevel) +} + +func TestGetLogLevel(t *testing.T) { + err := DefaultConfig.Load("../config_test.yml", false) + assert.Nil(t, err) + assert.Equal(t, "ERROR", GetLogLevel()) +} + +func TestGetJobAuthSecret(t *testing.T) { + os.Setenv("JOBSERVICE_SECRET", "test_job_secret") + assert.Equal(t, "test_job_secret", GetJobAuthSecret()) +} + +func TestGetUIAuthSecret(t *testing.T) { + os.Setenv("UI_SECRET", "test_ui_secret") + assert.Equal(t, "test_ui_secret", GetUIAuthSecret()) +} diff --git a/src/registryctl/config_test.yml b/src/registryctl/config_test.yml new file mode 100644 index 000000000..3b6fff786 --- /dev/null +++ b/src/registryctl/config_test.yml @@ -0,0 +1,8 @@ +--- +protocol: "http" +port: 1234 +log_level: "ERROR" + +https_config: + cert: "server.crt" + key: "server.key" \ No newline at end of file diff --git a/src/registryctl/handlers/handler.go b/src/registryctl/handlers/handler.go new file mode 100644 index 000000000..375d4af3a --- /dev/null +++ b/src/registryctl/handlers/handler.go @@ -0,0 +1,82 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handlers + +import ( + "net/http" + "os" + + gorilla_handlers "github.com/gorilla/handlers" + "github.com/vmware/harbor/src/common/utils/log" + "github.com/vmware/harbor/src/registryctl/auth" +) + +// NewHandlerChain returns a gorilla router which is wrapped by authenticate handler +// and logging handler +func NewHandlerChain() http.Handler { + h := newRouter() + secrets := map[string]string{ + "jobSecret": os.Getenv("JOBSERVICE_SECRET"), + } + insecureAPIs := map[string]bool{ + "/api/health": true, + } + h = newAuthHandler(auth.NewSecretHandler(secrets), h, insecureAPIs) + h = gorilla_handlers.LoggingHandler(os.Stdout, h) + return h +} + +type authHandler struct { + authenticator auth.AuthenticationHandler + handler http.Handler + insecureAPIs map[string]bool +} + +func newAuthHandler(authenticator auth.AuthenticationHandler, handler http.Handler, insecureAPIs map[string]bool) http.Handler { + return &authHandler{ + authenticator: authenticator, + handler: handler, + insecureAPIs: insecureAPIs, + } +} + +func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if a.authenticator == nil { + log.Errorf("No authenticator found in regsitry controller.") + http.Error(w, http.StatusText(http.StatusInternalServerError), + http.StatusInternalServerError) + return + } + + if a.insecureAPIs != nil && a.insecureAPIs[r.URL.Path] { + if a.handler != nil { + a.handler.ServeHTTP(w, r) + } + return + } + + err := a.authenticator.AuthorizeRequest(r) + if err != nil { + log.Errorf("failed to authenticate request: %v", err) + http.Error(w, http.StatusText(http.StatusUnauthorized), + http.StatusUnauthorized) + return + } + + if a.handler != nil { + a.handler.ServeHTTP(w, r) + } + return +} diff --git a/src/registryctl/handlers/handler_test.go b/src/registryctl/handlers/handler_test.go new file mode 100644 index 000000000..c6f6ade20 --- /dev/null +++ b/src/registryctl/handlers/handler_test.go @@ -0,0 +1,69 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handlers + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vmware/harbor/src/registryctl/auth" +) + +type fakeAuthenticator struct { + err error +} + +func (f *fakeAuthenticator) AuthorizeRequest(req *http.Request) error { + return f.err +} + +type fakeHandler struct { + responseCode int +} + +func (f *fakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(f.responseCode) +} + +func TestNewAuthHandler(t *testing.T) { + cases := []struct { + authenticator auth.AuthenticationHandler + handler http.Handler + insecureAPIs map[string]bool + responseCode int + requestURL string + }{ + {nil, nil, nil, http.StatusInternalServerError, "http://localhost/good"}, + {&fakeAuthenticator{err: nil}, nil, nil, http.StatusOK, "http://localhost/hello"}, + {&fakeAuthenticator{err: errors.New("error")}, nil, nil, http.StatusUnauthorized, "http://localhost/hello"}, + {&fakeAuthenticator{err: nil}, &fakeHandler{http.StatusNotFound}, nil, http.StatusNotFound, "http://localhost/notexsit"}, {&fakeAuthenticator{err: nil}, &fakeHandler{http.StatusOK}, map[string]bool{"/api/insecure": true}, http.StatusOK, "http://localhost/api/insecure"}, + } + + for _, c := range cases { + handler := newAuthHandler(c.authenticator, c.handler, c.insecureAPIs) + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", c.requestURL, nil) + handler.ServeHTTP(w, r) + assert.Equal(t, c.responseCode, w.Code, "unexpected response code") + } + handler := NewHandlerChain() + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "http://localhost/api/health", nil) + handler.ServeHTTP(w, r) + +} diff --git a/src/registryctl/handlers/router.go b/src/registryctl/handlers/router.go new file mode 100644 index 000000000..a481c8e5a --- /dev/null +++ b/src/registryctl/handlers/router.go @@ -0,0 +1,29 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handlers + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/vmware/harbor/src/registryctl/api" +) + +func newRouter() http.Handler { + r := mux.NewRouter() + r.HandleFunc("/api/registry/gc", api.StartGC).Methods("POST") + r.HandleFunc("/api/health", api.Health).Methods("GET") + return r +} diff --git a/src/registryctl/main.go b/src/registryctl/main.go new file mode 100644 index 000000000..c11818c5a --- /dev/null +++ b/src/registryctl/main.go @@ -0,0 +1,91 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "crypto/tls" + "flag" + "net/http" + + "github.com/vmware/harbor/src/common/utils/log" + "github.com/vmware/harbor/src/registryctl/config" + "github.com/vmware/harbor/src/registryctl/handlers" +) + +// RegistryCtl for registry controller +type RegistryCtl struct { + ServerConf config.Configuration + Handler http.Handler +} + +// Start the registry controller +func (s *RegistryCtl) Start() { + regCtl := &http.Server{ + Addr: ":" + s.ServerConf.Port, + Handler: s.Handler, + } + + if s.ServerConf.Protocol == "HTTPS" { + tlsCfg := &tls.Config{ + MinVersion: tls.VersionTLS12, + CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, + PreferServerCipherSuites: true, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + }, + } + + regCtl.TLSConfig = tlsCfg + regCtl.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0) + } + + var err error + if s.ServerConf.Protocol == "HTTPS" { + err = regCtl.ListenAndServeTLS(s.ServerConf.HTTPSConfig.Cert, s.ServerConf.HTTPSConfig.Key) + } else { + err = regCtl.ListenAndServe() + } + + if err != nil { + log.Fatal(err) + } + + return +} + +func main() { + + configPath := flag.String("c", "", "Specify the yaml config file path") + flag.Parse() + + if configPath == nil || len(*configPath) == 0 { + flag.Usage() + log.Fatal("Config file should be specified") + } + + if err := config.DefaultConfig.Load(*configPath, true); err != nil { + log.Fatalf("Failed to load configurations with error: %s\n", err) + } + + regCtl := &RegistryCtl{ + ServerConf: *config.DefaultConfig, + Handler: handlers.NewHandlerChain(), + } + + regCtl.Start() +}