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() +}