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.
This commit is contained in:
Yan 2018-07-16 16:50:28 +08:00 committed by GitHub
parent 8a92019e8e
commit d5b85a6748
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1247 additions and 78 deletions

View File

@ -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:

View File

@ -62,3 +62,4 @@ READ_ONLY=false
SKIP_RELOAD_ENV_PATTERN=$skip_reload_env_pattern
RELOAD_KEY=$reload_key
LDAP_GROUP_ADMIN_DN=$ldap_group_admin_dn
REGISTRY_CONTROLLER_URL=$registry_controller_url

View File

@ -0,0 +1,8 @@
---
protocol: "http"
port: 8080
log_level: "INFO"
#https_config:
# cert: "server.crt"
# key: "server.key"

View File

@ -0,0 +1,3 @@
UI_SECRET=$ui_secret
JOBSERVICE_SECRET=$jobservice_secret

View File

@ -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"]

View File

@ -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

View File

@ -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..."

View File

@ -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"]

View File

@ -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"

View File

@ -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":

View File

@ -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"
)

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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))
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}
}

View File

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

View File

@ -0,0 +1,8 @@
---
protocol: "http"
port: 1234
log_level: "ERROR"
https_config:
cert: "server.crt"
key: "server.key"

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

91
src/registryctl/main.go Normal file
View File

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