configure harbor

This commit is contained in:
Wenkai Yin 2016-12-30 18:04:01 +08:00
parent ad4da5f043
commit b62a958250
52 changed files with 2119 additions and 562 deletions

3
.gitignore vendored
View File

@ -1,8 +1,11 @@
harbor harbor
make/common/config/* make/common/config/*
make/dev/adminserver/harbor_adminserver
make/dev/ui/harbor_ui make/dev/ui/harbor_ui
make/dev/jobservice/harbor_jobservice make/dev/jobservice/harbor_jobservice
src/adminserver/adminserver
src/ui/ui src/ui/ui
src/jobservice/jobservice src/jobservice/jobservice
src/common/dao/dao.test
*.pyc *.pyc
jobservice/test jobservice/test

View File

@ -0,0 +1,37 @@
LOG_LEVEL=debug
EXT_ENDPOINT=$ui_url
AUTH_MODE=$auth_mode
SELF_REGISTRATION=$self_registration
LDAP_URL=$ldap_url
LDAP_SEARCH_DN=$ldap_searchdn
LDAP_SEARCH_PWD=$ldap_search_pwd
LDAP_BASE_DN=$ldap_basedn
LDAP_FILTER=$ldap_filter
LDAP_UID=$ldap_uid
LDAP_SCOPE=$ldap_scope
DATABASE_TYPE=mysql
MYSQL_HOST=mysql
MYSQL_PORT=3306
MYSQL_USR=root
MYSQL_PWD=$db_password
MYSQL_DATABASE=registry
REGISTRY_URL=http://registry:5000
TOKEN_SERVICE_URL=http://ui/service/token
EMAIL_HOST=$email_host
EMAIL_PORT=$email_port
EMAIL_USR=$email_usr
EMAIL_PWD=$email_pwd
EMAIL_TLS=$email_tls
EMAIL_FROM=$email_from
EMAIL_IDENTITY=$email_identity
HARBOR_ADMIN_PASSWORD=$harbor_admin_password
PROJECT_CREATION_RESTRICTION=$project_creation_restriction
VERIFY_REMOTE_CERT=$verify_remote_cert
MAX_JOB_WORKERS=$max_job_workers
LOG_DIR=/var/log/jobs
UI_SECRET=$ui_secret
SECRET_KEY=$secret_key
TOKEN_EXPIRATION=$token_expiration
CFG_EXPIRATION=$cfg_expiration
USE_COMPRESSED_JS=$use_compressed_js
GODEBUG=netdns=cgo

View File

@ -1,15 +1,5 @@
MYSQL_HOST=mysql
MYSQL_PORT=3306
MYSQL_USR=root
MYSQL_PWD=$db_password
UI_SECRET=$ui_secret
SECRET_KEY=$secret_key
CONFIG_PATH=/etc/jobservice/app.conf
REGISTRY_URL=http://registry:5000
VERIFY_REMOTE_CERT=$verify_remote_cert
MAX_JOB_WORKERS=$max_job_workers
LOG_LEVEL=debug LOG_LEVEL=debug
LOG_DIR=/var/log/jobs UI_SECRET=$ui_secret
CONFIG_PATH=/etc/jobservice/app.conf
MAX_JOB_WORKERS=$max_job_workers
GODEBUG=netdns=cgo GODEBUG=netdns=cgo
EXT_ENDPOINT=$ui_url
TOKEN_ENDPOINT=http://ui

View File

@ -6,13 +6,4 @@ types = en-US|zh-CN
names = en-US|zh-CN names = en-US|zh-CN
[dev] [dev]
httpport = 80 httpport = 80
[mail]
identity = $email_identity
host = $email_server
port = $email_server_port
username = $email_username
password = $email_password
from = $email_from
ssl = $email_ssl

View File

@ -1,29 +1,4 @@
MYSQL_HOST=mysql
MYSQL_PORT=3306
MYSQL_USR=root
MYSQL_PWD=$db_password
REGISTRY_URL=http://registry:5000
JOB_SERVICE_URL=http://jobservice
UI_URL=http://ui
CONFIG_PATH=/etc/ui/app.conf
EXT_REG_URL=$hostname
HARBOR_ADMIN_PASSWORD=$harbor_admin_password
AUTH_MODE=$auth_mode
LDAP_URL=$ldap_url
LDAP_SEARCH_DN=$ldap_searchdn
LDAP_SEARCH_PWD=$ldap_search_pwd
LDAP_BASE_DN=$ldap_basedn
LDAP_FILTER=$ldap_filter
LDAP_UID=$ldap_uid
LDAP_SCOPE=$ldap_scope
UI_SECRET=$ui_secret
SECRET_KEY=$secret_key
SELF_REGISTRATION=$self_registration
USE_COMPRESSED_JS=$use_compressed_js
LOG_LEVEL=debug LOG_LEVEL=debug
CONFIG_PATH=/etc/ui/app.conf
UI_SECRET=$ui_secret
GODEBUG=netdns=cgo GODEBUG=netdns=cgo
EXT_ENDPOINT=$ui_url
TOKEN_ENDPOINT=http://ui
VERIFY_REMOTE_CERT=$verify_remote_cert
TOKEN_EXPIRATION=$token_expiration
PROJECT_CREATION_RESTRICTION=$project_creation_restriction

View File

@ -0,0 +1,12 @@
FROM golang:1.7.3
MAINTAINER yinw@vmware.com
COPY . /go/src/github.com/vmware/harbor
WORKDIR /go/src/github.com/vmware/harbor/src/adminserver
RUN go build -v -a -o /go/bin/harbor_adminserver \
&& chmod u+x /go/bin/harbor_adminserver
WORKDIR /go/bin/
ENTRYPOINT ["/go/bin/harbor_adminserver"]

View File

@ -40,6 +40,22 @@ services:
options: options:
syslog-address: "tcp://127.0.0.1:1514" syslog-address: "tcp://127.0.0.1:1514"
tag: "mysql" tag: "mysql"
adminserver:
build:
context: ../../
dockerfile: make/dev/adminserver/Dockerfile
env_file:
- ../common/config/adminserver/env
restart: always
volumes:
- /data/config/:/etc/harbor/
depends_on:
- log
logging:
driver: "syslog"
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "adminserver"
ui: ui:
build: build:
context: ../../ context: ../../
@ -52,6 +68,8 @@ services:
- ../common/config/ui/private_key.pem:/etc/ui/private_key.pem - ../common/config/ui/private_key.pem:/etc/ui/private_key.pem
depends_on: depends_on:
- log - log
- adminserver
- registry
logging: logging:
driver: "syslog" driver: "syslog"
options: options:
@ -69,6 +87,7 @@ services:
- ../common/config/jobservice/app.conf:/etc/jobservice/app.conf - ../common/config/jobservice/app.conf:/etc/jobservice/app.conf
depends_on: depends_on:
- ui - ui
- adminserver
logging: logging:
driver: "syslog" driver: "syslog"
options: options:

View File

@ -41,6 +41,21 @@ services:
options: options:
syslog-address: "tcp://127.0.0.1:1514" syslog-address: "tcp://127.0.0.1:1514"
tag: "mysql" tag: "mysql"
adminserver:
image: vmware/harbor-adminserver
container_name: harbor-adminserver
env_file:
- ./common/config/adminserver/env
restart: always
volumes:
- /data/config/:/etc/harbor/
depends_on:
- log
logging:
driver: "syslog"
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "adminserver"
ui: ui:
image: vmware/harbor-ui image: vmware/harbor-ui
container_name: harbor-ui container_name: harbor-ui
@ -53,6 +68,8 @@ services:
- /data:/harbor_storage - /data:/harbor_storage
depends_on: depends_on:
- log - log
- adminserver
- registry
logging: logging:
driver: "syslog" driver: "syslog"
options: options:
@ -69,6 +86,7 @@ services:
- ./common/config/jobservice/app.conf:/etc/jobservice/app.conf - ./common/config/jobservice/app.conf:/etc/jobservice/app.conf
depends_on: depends_on:
- ui - ui
- adminserver
logging: logging:
driver: "syslog" driver: "syslog"
options: options:

View File

@ -0,0 +1,8 @@
FROM library/photon:1.0
RUN mkdir /harbor/
COPY ./make/dev/adminserver/harbor_adminserver /harbor/
RUN chmod u+x /harbor/harbor_adminserver
WORKDIR /harbor/
ENTRYPOINT ["/harbor/harbor_adminserver"]

View File

@ -0,0 +1,29 @@
/*
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api
import (
"net/http"
)
func handleInternalServerError(w http.ResponseWriter) {
http.Error(w, http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError)
}
func handleBadRequestError(w http.ResponseWriter, error string) {
http.Error(w, error, http.StatusBadRequest)
}

161
src/adminserver/api/cfg.go Normal file
View File

@ -0,0 +1,161 @@
/*
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api
import (
"encoding/json"
"io/ioutil"
"net/http"
"strconv"
cfg "github.com/vmware/harbor/src/adminserver/systemcfg"
comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
)
// ListCfgs lists configurations
func ListCfgs(w http.ResponseWriter, r *http.Request) {
cfg, err := cfg.GetSystemCfg()
if err != nil {
log.Errorf("failed to get system configurations: %v", err)
handleInternalServerError(w)
return
}
b, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
log.Errorf("failed to marshal configurations: %v", err)
handleInternalServerError(w)
return
}
if _, err = w.Write(b); err != nil {
log.Errorf("failed to write response: %v", err)
}
}
// UpdateCfgs updates configurations
func UpdateCfgs(w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Errorf("failed to read request body: %v", err)
handleInternalServerError(w)
return
}
m := &map[string]string{}
if err = json.Unmarshal(b, m); err != nil {
handleBadRequestError(w, err.Error())
return
}
log.Info(m)
system, err := cfg.GetSystemCfg()
if err != nil {
handleInternalServerError(w)
return
}
if err := populate(system, *m); err != nil {
log.Errorf("failed to populate system configurations: %v", err)
handleInternalServerError(w)
return
}
log.Info(system.Authentication.SelfRegistration)
if err = cfg.UpdateSystemCfg(system); err != nil {
log.Errorf("failed to update system configurations: %v", err)
handleInternalServerError(w)
return
}
}
// populate attrs of cfg according to m
func populate(cfg *models.SystemCfg, m map[string]string) error {
if mode, ok := m[comcfg.AUTH_MODE]; ok {
cfg.Authentication.Mode = mode
}
if value, ok := m[comcfg.SELF_REGISTRATION]; ok {
cfg.Authentication.SelfRegistration = value == "true"
}
if url, ok := m[comcfg.LDAP_URL]; ok {
cfg.Authentication.LDAP.URL = url
}
if dn, ok := m[comcfg.LDAP_SEARCH_DN]; ok {
cfg.Authentication.LDAP.SearchDN = dn
}
if pwd, ok := m[comcfg.LDAP_SEARCH_PWD]; ok {
cfg.Authentication.LDAP.SearchPwd = pwd
}
if dn, ok := m[comcfg.LDAP_BASE_DN]; ok {
cfg.Authentication.LDAP.BaseDN = dn
}
if uid, ok := m[comcfg.LDAP_UID]; ok {
cfg.Authentication.LDAP.UID = uid
}
if filter, ok := m[comcfg.LDAP_FILTER]; ok {
cfg.Authentication.LDAP.Filter = filter
}
if scope, ok := m[comcfg.LDAP_SCOPE]; ok {
i, err := strconv.Atoi(scope)
if err != nil {
return err
}
cfg.Authentication.LDAP.Scope = i
}
if value, ok := m[comcfg.EMAIL_SERVER]; ok {
cfg.Email.Host = value
}
if value, ok := m[comcfg.EMAIL_SERVER_PORT]; ok {
cfg.Email.Port = value
}
if value, ok := m[comcfg.EMAIL_USERNAME]; ok {
cfg.Email.Username = value
}
if value, ok := m[comcfg.EMAIL_PWD]; ok {
cfg.Email.Host = value
}
if value, ok := m[comcfg.EMAIL_SSL]; ok {
cfg.Email.Password = value
}
if value, ok := m[comcfg.EMAIL_FROM]; ok {
cfg.Email.From = value
}
if value, ok := m[comcfg.EMAIL_IDENTITY]; ok {
cfg.Email.Identity = value
}
if value, ok := m[comcfg.PROJECT_CREATION_RESTRICTION]; ok {
cfg.ProjectCreationRestriction = value
}
if value, ok := m[comcfg.VERIFY_REMOTE_CERT]; ok {
cfg.VerifyRemoteCert = value == "true"
}
if value, ok := m[comcfg.MAX_JOB_WORKERS]; ok {
if i, err := strconv.Atoi(value); err != nil {
return err
} else {
cfg.MaxJobWorkers = i
}
}
return nil
}

60
src/adminserver/main.go Normal file
View File

@ -0,0 +1,60 @@
/*
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"net/http"
"os"
cfg "github.com/vmware/harbor/src/adminserver/systemcfg"
"github.com/vmware/harbor/src/common/utils/log"
)
// Server for admin component
type Server struct {
Port string
Handler http.Handler
}
// Serve the API
func (s *Server) Serve() error {
server := &http.Server{
Addr: ":" + s.Port,
Handler: s.Handler,
}
return server.ListenAndServe()
}
func main() {
log.Info("initializing system configurations...")
if err := cfg.Init(); err != nil {
log.Fatalf("failed to initialize the system: %v", err)
}
log.Info("system initialization completed")
port := os.Getenv("PORT")
if len(port) == 0 {
port = "80"
}
server := &Server{
Port: port,
Handler: newHandler(),
}
if err := server.Serve(); err != nil {
log.Fatal(err)
}
}

30
src/adminserver/router.go Normal file
View File

@ -0,0 +1,30 @@
/*
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"net/http"
"github.com/gorilla/mux"
"github.com/vmware/harbor/src/adminserver/api"
)
func newHandler() http.Handler {
r := mux.NewRouter()
r.HandleFunc("/api/configurations", api.ListCfgs).Methods("GET")
r.HandleFunc("/api/configurations", api.UpdateCfgs).Methods("PUT")
return r
}

View File

@ -0,0 +1,30 @@
/*
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package systemcfg
import (
"github.com/vmware/harbor/src/common/models"
)
// Driver defines methods that a configuration store driver must implement
type Driver interface {
// Name returns a human-readable name of the driver
Name() string
// Read reads the configurations from store
Read() (*models.SystemCfg, error)
// Write writes the configurations to store
Write(*models.SystemCfg) error
}

View File

@ -0,0 +1,104 @@
/*
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package systemcfg
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"sync"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
)
const (
// the default path of configuration file
defaultPath = "/etc/harbor/config.json"
)
type cfgStore struct {
path string // the path of cfg file
sync.RWMutex
}
// NewCfgStore returns an instance of cfgStore that stores the configurations
// in a json file. The file will be created if it does not exist.
func NewCfgStore(path ...string) (Driver, error) {
p := defaultPath
if len(path) != 0 {
p = path[0]
}
if _, err := os.Stat(p); os.IsNotExist(err) {
log.Infof("the configuration file %s does not exist, creating it...", p)
if err = os.MkdirAll(filepath.Dir(p), 0600); err != nil {
return nil, err
}
if err = ioutil.WriteFile(p, []byte{}, 0600); err != nil {
return nil, err
}
}
return &cfgStore{
path: p,
}, nil
}
// Name ...
func (c *cfgStore) Name() string {
return "JSON"
}
// Read ...
func (c *cfgStore) Read() (*models.SystemCfg, error) {
c.RLock()
defer c.RUnlock()
b, err := ioutil.ReadFile(c.path)
if err != nil {
return nil, err
}
// empty file
if len(b) == 0 {
return nil, nil
}
config := &models.SystemCfg{}
if err = json.Unmarshal(b, config); err != nil {
return nil, err
}
return config, nil
}
// Write ...
func (c *cfgStore) Write(config *models.SystemCfg) error {
b, err := json.MarshalIndent(config, "", " ")
if err != nil {
return err
}
c.Lock()
defer c.Unlock()
if err = ioutil.WriteFile(c.path, b, 0600); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,50 @@
/*
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package systemcfg
import (
"os"
"testing"
)
func TestReadWrite(t *testing.T) {
path := "/tmp/config.json"
store, err := NewCfgStore(path)
if err != nil {
t.Fatalf("failed to create json cfg store: %v", err)
}
defer func() {
if err := os.Remove(path); err != nil {
t.Fatalf("failed to remove the json file %s: %v", path, err)
}
}()
config := &cfg.SystemCfg{
Authentication: &cfg.Authentication{
LDAP: &cfg.LDAP{},
},
Database: &cfg.Database{
MySQL: &cfg.MySQL{},
},
}
if err := store.Write(config); err != nil {
t.Fatalf("failed to write configurations to json file: %v", err)
}
if _, err = store.Read(); err != nil {
t.Fatalf("failed to read configurations from json file: %v", err)
}
}

View File

@ -0,0 +1,176 @@
/*
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package systemcfg
import (
"fmt"
"os"
"strconv"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
)
var store Driver
// Init system configurations. Read from config store first, if null read from env
func Init() (err error) {
s := getCfgStore()
switch s {
case "json":
store, err = NewCfgStore()
if err != nil {
return
}
default:
return fmt.Errorf("unsupported configuration store driver %s", s)
}
log.Infof("configuration store driver: %s", store.Name())
cfg, err := store.Read()
if err != nil {
return err
}
if cfg == nil {
log.Info("configurations read from store driver are null, initializing system from environment variables...")
cfg, err = initFromEnv()
if err != nil {
return err
}
} else {
//read the following attrs from env every time boots up,
//and sync them into cfg store
cfg.DomainName = os.Getenv("EXT_ENDPOINT")
cfg.Database.MySQL.Password = os.Getenv("MYSQL_PWD")
cfg.JobLogDir = os.Getenv("LOG_DIR")
cfg.CompressJS = os.Getenv("USE_COMPRESSED_JS") == "on"
exp, err := strconv.Atoi(os.Getenv("TOKEN_EXPIRATION"))
if err != nil {
return err
}
cfg.TokenExpiration = exp
cfg.SecretKey = os.Getenv("SECRET_KEY")
cfgExp, err := strconv.Atoi(os.Getenv("CFG_EXPIRATION"))
if err != nil {
return err
}
cfg.CfgExpiration = cfgExp
}
if err = store.Write(cfg); err != nil {
return err
}
return nil
}
func getCfgStore() string {
return "json"
}
func initFromEnv() (*models.SystemCfg, error) {
cfg := &models.SystemCfg{}
cfg.DomainName = os.Getenv("EXT_ENDPOINT")
cfg.Authentication = &models.Authentication{
Mode: os.Getenv("AUTH_MODE"),
SelfRegistration: os.Getenv("SELF_REGISTRATION") == "true",
LDAP: &models.LDAP{
URL: os.Getenv("LDAP_URL"),
SearchDN: os.Getenv("LDAP_SEARCH_DN"),
SearchPwd: os.Getenv("LDAP_SEARCH_PWD"),
BaseDN: os.Getenv("LDAP_BASE_DN"),
Filter: os.Getenv("LDAP_FILTER"),
UID: os.Getenv("LDAP_UID"),
},
}
scope, err := strconv.Atoi(os.Getenv("LDAP_SCOPE"))
if err != nil {
return nil, err
}
cfg.Authentication.LDAP.Scope = scope
cfg.Database = &models.Database{
Type: os.Getenv("DATABASE_TYPE"),
MySQL: &models.MySQL{
Host: os.Getenv("MYSQL_HOST"),
Username: os.Getenv("MYSQL_USR"),
Password: os.Getenv("MYSQL_PWD"),
Database: os.Getenv("MYSQL_DATABASE"),
},
SQLite: &models.SQLite{
File: os.Getenv("SQLITE_FILE"),
},
}
port, err := strconv.Atoi(os.Getenv("MYSQL_PORT"))
if err != nil {
return nil, err
}
cfg.Database.MySQL.Port = port
cfg.TokenService = &models.TokenService{
URL: os.Getenv("TOKEN_SERVICE_URL"),
}
cfg.Registry = &models.Registry{
URL: os.Getenv("REGISTRY_URL"),
}
cfg.Email = &models.Email{
Host: os.Getenv("EMAIL_HOST"),
Port: os.Getenv("EMAIL_PORT"),
Username: os.Getenv("EMAIL_USR"),
Password: os.Getenv("EMAIL_PWD"),
TLS: os.Getenv("EMAIL_TLS") == "true",
From: os.Getenv("EMAIL_FROM"),
Identity: os.Getenv("EMAIL_IDENTITY"),
}
cfg.VerifyRemoteCert = os.Getenv("VERIFY_REMOTE_CERT") == "true"
cfg.ProjectCreationRestriction = os.Getenv("PROJECT_CREATION_RESTRICTION")
workers, err := strconv.Atoi(os.Getenv("MAX_JOB_WORKERS"))
if err != nil {
return nil, err
}
cfg.MaxJobWorkers = workers
cfg.JobLogDir = os.Getenv("LOG_DIR")
cfg.InitialAdminPwd = os.Getenv("HARBOR_ADMIN_PASSWORD")
cfg.CompressJS = os.Getenv("USE_COMPRESSED_JS") == "on"
tokenExp, err := strconv.Atoi(os.Getenv("TOKEN_EXPIRATION"))
if err != nil {
return nil, err
}
cfg.TokenExpiration = tokenExp
cfg.SecretKey = os.Getenv("SECRET_KEY")
cfgExp, err := strconv.Atoi(os.Getenv("CFG_EXPIRATION"))
if err != nil {
return nil, err
}
cfg.CfgExpiration = cfgExp
return cfg, nil
}
// GetSystemCfg returns the system configurations
func GetSystemCfg() (*models.SystemCfg, error) {
return store.Read()
}
// UpdateSystemCfg updates the system configurations
func UpdateSystemCfg(cfg *models.SystemCfg) error {
return store.Write(cfg)
}

View File

@ -22,11 +22,11 @@ import (
"strconv" "strconv"
"github.com/astaxie/beego/validation" "github.com/astaxie/beego/validation"
"github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/auth" "github.com/vmware/harbor/src/ui/auth"
"github.com/vmware/harbor/src/ui/config"
"github.com/astaxie/beego" "github.com/astaxie/beego"
) )
@ -212,6 +212,10 @@ func (b *BaseAPI) GetPaginationParams() (page, pageSize int64) {
} }
// GetIsInsecure ... // GetIsInsecure ...
func GetIsInsecure() bool { func GetIsInsecure() (bool, error) {
return !config.VerifyRemoteCert() verify, err := config.VerifyRemoteCert()
if err != nil {
return false, err
}
return !verify, nil
} }

View File

@ -17,162 +17,119 @@
package config package config
import ( import (
"bytes"
"fmt" "fmt"
"os" "io/ioutil"
"net/http"
"strings" "strings"
"github.com/astaxie/beego/cache"
"github.com/vmware/harbor/src/common/utils"
) )
// ConfLoader is the interface to load configurations const (
type ConfLoader interface { //auth mode
// Load will load configuration from different source into a string map, the values in the map will be parsed in to configurations. DB_AUTH = "db_auth"
Load() (map[string]string, error) LDAP_AUTH = "ldap_auth"
//project_creation_restriction
PRO_CRT_RESTR_EVERYONE = "everyone"
PRO_CRT_RESTR_ADM_ONLY = "adminonly"
LDAP_SCOPE_BASE = "1"
LDAP_SCOPE_ONELEVEL = "2"
LDAP_SCOPE_SUBTREE = "3"
AUTH_MODE = "auth_mode"
SELF_REGISTRATION = "self_registration"
LDAP_URL = "ldap_url"
LDAP_SEARCH_DN = "ldap_search_dn"
LDAP_SEARCH_PWD = "ldap_search_pwd"
LDAP_BASE_DN = "ldap_base_dn"
LDAP_UID = "ldap_uid"
LDAP_FILTER = "ldap_filter"
LDAP_SCOPE = "ldap_scope"
EMAIL_SERVER = "email_server"
EMAIL_SERVER_PORT = "email_server_port"
EMAIL_USERNAME = "email_server_username"
EMAIL_PWD = "email_server_pwd"
EMAIL_FROM = "email_from"
EMAIL_SSL = "email_ssl"
EMAIL_IDENTITY = "email_identity"
PROJECT_CREATION_RESTRICTION = "project_creation_restriction"
VERIFY_REMOTE_CERT = "verify_remote_cert"
MAX_JOB_WORKERS = "max_job_workers"
CFG_EXPIRATION = "cfg_expiration"
)
type Manager struct {
Key string
Cache cache.Cache
Loader *Loader
} }
// EnvConfigLoader loads the config from env vars. func NewManager(key, url string) *Manager {
type EnvConfigLoader struct { return &Manager{
Keys []string Key: key,
} Cache: cache.NewMemoryCache(),
Loader: NewLoader(url),
// Load ...
func (ec *EnvConfigLoader) Load() (map[string]string, error) {
m := make(map[string]string)
for _, k := range ec.Keys {
m[k] = os.Getenv(k)
} }
return m, nil
} }
// ConfParser ... func (m *Manager) GetFromCache() interface{} {
type ConfParser interface { value := m.Cache.Get(m.Key)
if value != nil {
//Parse parse the input raw map into a config map return value
Parse(raw map[string]string, config map[string]interface{}) error
}
// Config wraps a map for the processed configuration values,
// and loader parser to read configuration from external source and process the values.
type Config struct {
Config map[string]interface{}
Loader ConfLoader
Parser ConfParser
}
// Load reload the configurations
func (conf *Config) Load() error {
rawMap, err := conf.Loader.Load()
if err != nil {
return err
} }
err = conf.Parser.Parse(rawMap, conf.Config)
return err
}
// MySQLSetting wraps the settings of a MySQL DB
type MySQLSetting struct {
Database string
User string
Password string
Host string
Port string
}
// SQLiteSetting wraps the settings of a SQLite DB
type SQLiteSetting struct {
FilePath string
}
type commonParser struct{}
// Parse parses the db settings, veryfy_remote_cert, ext_endpoint, token_endpoint
func (cp *commonParser) Parse(raw map[string]string, config map[string]interface{}) error {
db := strings.ToLower(raw["DATABASE"])
if db == "mysql" || db == "" {
db = "mysql"
mySQLDB := raw["MYSQL_DATABASE"]
if len(mySQLDB) == 0 {
mySQLDB = "registry"
}
setting := MySQLSetting{
mySQLDB,
raw["MYSQL_USR"],
raw["MYSQL_PWD"],
raw["MYSQL_HOST"],
raw["MYSQL_PORT"],
}
config["mysql"] = setting
} else if db == "sqlite" {
f := raw["SQLITE_FILE"]
if len(f) == 0 {
f = "registry.db"
}
setting := SQLiteSetting{
f,
}
config["sqlite"] = setting
} else {
return fmt.Errorf("Invalid DB: %s", db)
}
config["database"] = db
//By default it's true
config["verify_remote_cert"] = raw["VERIFY_REMOTE_CERT"] != "off"
config["ext_endpoint"] = raw["EXT_ENDPOINT"]
config["token_endpoint"] = raw["TOKEN_ENDPOINT"]
config["log_level"] = raw["LOG_LEVEL"]
return nil return nil
} }
var commonConfig *Config type Loader struct {
url string
client *http.Client
}
func init() { func NewLoader(url string) *Loader {
commonKeys := []string{"DATABASE", "MYSQL_DATABASE", "MYSQL_USR", "MYSQL_PWD", "MYSQL_HOST", "MYSQL_PORT", "SQLITE_FILE", "VERIFY_REMOTE_CERT", "EXT_ENDPOINT", "TOKEN_ENDPOINT", "LOG_LEVEL"} return &Loader{
commonConfig = &Config{ url: url,
Config: make(map[string]interface{}), client: &http.Client{},
Loader: &EnvConfigLoader{Keys: commonKeys},
Parser: &commonParser{},
}
if err := commonConfig.Load(); err != nil {
panic(err)
} }
} }
// Reload will reload the configuration. func (l *Loader) Init() error {
func Reload() error { addr := l.url
return commonConfig.Load() if strings.Contains(addr, "://") {
addr = strings.Split(addr, "://")[1]
}
return utils.TestTCPConn(addr, 60, 2)
} }
// Database returns the DB type in configuration. func (l *Loader) Load() ([]byte, error) {
func Database() string { resp, err := l.client.Get(l.url + "/api/configurations")
return commonConfig.Config["database"].(string) if err != nil {
return nil, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return b, nil
} }
// MySQL returns the mysql setting in configuration. func (l *Loader) Upload(b []byte) error {
func MySQL() MySQLSetting { req, err := http.NewRequest("PUT", l.url+"/api/configurations", bytes.NewReader(b))
return commonConfig.Config["mysql"].(MySQLSetting) if err != nil {
} return err
}
resp, err := l.client.Do(req)
if err != nil {
return err
}
// SQLite returns the SQLite setting if resp.StatusCode != http.StatusOK {
func SQLite() SQLiteSetting { return fmt.Errorf("unexpected http status code: %d", resp.StatusCode)
return commonConfig.Config["sqlite"].(SQLiteSetting) }
}
// VerifyRemoteCert returns bool value. return nil
func VerifyRemoteCert() bool {
return commonConfig.Config["verify_remote_cert"].(bool)
}
// ExtEndpoint ...
func ExtEndpoint() string {
return commonConfig.Config["ext_endpoint"].(string)
}
// TokenEndpoint returns the endpoint string of token service, which can be accessed by internal service of Harbor.
func TokenEndpoint() string {
return commonConfig.Config["token_endpoint"].(string)
}
// LogLevel returns the log level in string format.
func LogLevel() string {
return commonConfig.Config["log_level"].(string)
} }

View File

@ -17,11 +17,12 @@ package dao
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"sync" "sync"
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
"github.com/vmware/harbor/src/common/config" "github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
) )
@ -39,27 +40,32 @@ type Database interface {
} }
// InitDatabase initializes the database // InitDatabase initializes the database
func InitDatabase() { func InitDatabase(database *models.Database) error {
database, err := getDatabase() db, err := getDatabase(database)
if err != nil { if err != nil {
panic(err) return err
} }
log.Infof("initializing database: %s", database.String()) log.Infof("initializing database: %s", db.String())
if err := database.Register(); err != nil { if err := db.Register(); err != nil {
panic(err) return err
} }
log.Info("initialize database completed")
return nil
} }
func getDatabase() (db Database, err error) { func getDatabase(database *models.Database) (db Database, err error) {
switch config.Database() { switch database.Type {
case "", "mysql": case "", "mysql":
db = NewMySQL(config.MySQL().Host, config.MySQL().Port, config.MySQL().User, db = NewMySQL(database.MySQL.Host,
config.MySQL().Password, config.MySQL().Database) strconv.Itoa(database.MySQL.Port),
database.MySQL.Username,
database.MySQL.Password,
database.MySQL.Database)
case "sqlite": case "sqlite":
db = NewSQLite(config.SQLite().FilePath) db = NewSQLite(database.SQLite.File)
default: default:
err = fmt.Errorf("invalid database: %s", config.Database()) err = fmt.Errorf("invalid database: %s", database.Type)
} }
return return
} }

32
src/common/dao/config.go Normal file
View File

@ -0,0 +1,32 @@
/*
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dao
import (
"github.com/vmware/harbor/src/common/models"
)
// AuthModeCanBeModified determines whether auth mode can be
// modified or not. Auth mode can modified when there is only admin
// user in database.
func AuthModeCanBeModified() (bool, error) {
c, err := GetOrmer().QueryTable(&models.User{}).Count()
if err != nil {
return false, err
}
return c == 1, nil
}

View File

@ -0,0 +1,236 @@
/*
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dao
/*
import (
"testing"
"github.com/vmware/harbor/src/common/models"
)
func deleteConfigByKey(key string) error {
if _, err := GetOrmer().Raw("delete from properties where k = ?", key).
Exec(); err != nil {
return err
}
return nil
}
func TestGetConfigByKey(t *testing.T) {
cfg := &models.Config{
Key: "key",
Value: "value",
}
if err := InsertConfig(cfg); err != nil {
t.Fatalf("failed to insert configuration into table: %v", err)
}
defer func(key string) {
if err := deleteConfigByKey(key); err != nil {
t.Fatalf("failed to delete configuration %s: %v", key, err)
}
}(cfg.Key)
config, err := GetConfigByKey(cfg.Key)
if err != nil {
t.Fatalf("failed to get configuration by key %s: %v", cfg.Key, err)
}
if config == nil {
t.Fatal("configuration is nil")
}
if config.Value != cfg.Value {
t.Fatalf("unexpected value: %s != %s", config.Value, cfg.Value)
}
}
func TestListConfigs(t *testing.T) {
configs, err := ListConfigs()
if err != nil {
t.Fatalf("failed to list configurations: %v", err)
}
size := len(configs)
cfg := &models.Config{
Key: "key",
Value: "value",
}
if err := InsertConfig(cfg); err != nil {
t.Fatalf("failed to insert configuration into table: %v", err)
}
defer func(key string) {
if err := deleteConfigByKey(key); err != nil {
t.Fatalf("failed to delete configuration %s: %v", key, err)
}
}(cfg.Key)
configs, err = ListConfigs()
if err != nil {
t.Fatalf("failed to list configurations: %v", err)
}
if size+1 != len(configs) {
t.Fatalf("unexpected length of configurations: %d != %d", len(configs), size+1)
}
}
func TestInsertConfig(t *testing.T) {
cfg := &models.Config{
Key: "key1",
Value: "value1",
}
if err := InsertConfig(cfg); err != nil {
t.Fatalf("failed to insert configuration into table: %v", err)
}
defer func(key string) {
if err := deleteConfigByKey(key); err != nil {
t.Fatalf("failed to delete configuration %s: %v", key, err)
}
}(cfg.Key)
config, err := GetConfigByKey(cfg.Key)
if err != nil {
t.Fatalf("failed to get configuration by key %s: %v", cfg.Key, err)
}
if config == nil {
t.Fatal("configuration is nil")
}
if config.Value != cfg.Value {
t.Fatalf("unexpected value: %s != %s", config.Value, cfg.Value)
}
}
func TestUpdateConfig(t *testing.T) {
cfg := &models.Config{
Key: "key",
Value: "value",
}
if err := InsertConfig(cfg); err != nil {
t.Fatalf("failed to insert configuration into table: %v", err)
}
defer func(key string) {
if err := deleteConfigByKey(key); err != nil {
t.Fatalf("failed to delete configuration %s: %v", key, err)
}
}(cfg.Key)
newCfg := &models.Config{
Key: "key",
Value: "new_value",
}
if err := UpdateConfig(newCfg); err != nil {
t.Fatalf("failed to update configuration: %v", err)
}
config, err := GetConfigByKey(cfg.Key)
if err != nil {
t.Fatalf("failed to get configuration by key %s: %v", cfg.Key, err)
}
if config == nil {
t.Fatal("configuration is nil")
}
if config.Value != newCfg.Value {
t.Fatalf("unexpected value: %s != %s", config.Value, newCfg.Value)
}
}
func TestInsertOrUpdateConfigs(t *testing.T) {
cfg1 := &models.Config{
Key: "key1",
Value: "value1",
}
if err := InsertConfig(cfg1); err != nil {
t.Fatalf("failed to insert configuration into table: %v", err)
}
defer func(key string) {
if err := deleteConfigByKey(key); err != nil {
t.Fatalf("failed to delete configuration %s: %v", key, err)
}
}(cfg1.Key)
cfg2 := &models.Config{
Key: "key2",
Value: "value2",
}
if err := InsertOrUpdateConfigs([]*models.Config{cfg1, cfg2}); err != nil {
t.Fatalf("failed to insert or update configurations: %v", err)
}
defer func(key string) {
if err := deleteConfigByKey(key); err != nil {
t.Fatalf("failed to delete configuration %s: %v", key, err)
}
}(cfg2.Key)
}
func TestAuthModeCanBeModified(t *testing.T) {
c, err := GetOrmer().QueryTable(&models.User{}).Count()
if err != nil {
t.Fatalf("failed to count users: %v", err)
}
if c == 1 {
flag, err := AuthModeCanBeModified()
if err != nil {
t.Fatalf("failed to determine whether auth mode can be modified: %v", err)
}
if !flag {
t.Errorf("unexpected result: %t != %t", flag, true)
}
user := models.User{
Username: "user_for_config_test",
Email: "user_for_config_test@vmware.com",
Password: "P@ssword",
Realname: "user_for_config_test",
}
id, err := Register(user)
if err != nil {
t.Fatalf("failed to register user: %v", err)
}
defer func(id int64) {
if err := deleteUser(id); err != nil {
t.Fatalf("failed to delete user %d: %v", id, err)
}
}(id)
flag, err = AuthModeCanBeModified()
if err != nil {
t.Fatalf("failed to determine whether auth mode can be modified: %v", err)
}
if flag {
t.Errorf("unexpected result: %t != %t", flag, false)
}
} else {
flag, err := AuthModeCanBeModified()
if err != nil {
t.Fatalf("failed to determine whether auth mode can be modified: %v", err)
}
if flag {
t.Errorf("unexpected result: %t != %t", flag, false)
}
}
}
*/

View File

@ -17,10 +17,12 @@ package dao
import ( import (
"os" "os"
"strconv"
"testing" "testing"
"time" "time"
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
//"github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils" "github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
@ -42,7 +44,7 @@ func execUpdate(o orm.Ormer, sql string, params ...interface{}) error {
func clearUp(username string) { func clearUp(username string) {
var err error var err error
o := orm.NewOrm() o := GetOrmer()
o.Begin() o.Begin()
err = execUpdate(o, `delete err = execUpdate(o, `delete
@ -156,53 +158,63 @@ func TestMain(m *testing.M) {
} }
func testForMySQL(m *testing.M) int { func testForMySQL(m *testing.M) int {
db := os.Getenv("DATABASE") dbHost := os.Getenv("MYSQL_HOST")
defer os.Setenv("DATABASE", db)
os.Setenv("DATABASE", "mysql")
dbHost := os.Getenv("DB_HOST")
if len(dbHost) == 0 { if len(dbHost) == 0 {
log.Fatalf("environment variable DB_HOST is not set") log.Fatalf("environment variable MYSQL_HOST is not set")
} }
dbUser := os.Getenv("DB_USR") dbUser := os.Getenv("MYSQL_USR")
if len(dbUser) == 0 { if len(dbUser) == 0 {
log.Fatalf("environment variable DB_USR is not set") log.Fatalf("environment variable MYSQL_USR is not set")
} }
dbPort := os.Getenv("DB_PORT") dbPortStr := os.Getenv("MYSQL_PORT")
if len(dbPort) == 0 { if len(dbPortStr) == 0 {
log.Fatalf("environment variable DB_PORT is not set") log.Fatalf("environment variable MYSQL_PORT is not set")
}
dbPort, err := strconv.Atoi(dbPortStr)
if err != nil {
log.Fatalf("invalid MYSQL_PORT: %v", err)
} }
dbPassword := os.Getenv("DB_PWD")
log.Infof("DB_HOST: %s, DB_USR: %s, DB_PORT: %s, DB_PWD: %s\n", dbHost, dbUser, dbPort, dbPassword) dbPassword := os.Getenv("MYSQL_PWD")
dbDatabase := os.Getenv("MYSQL_DATABASE")
if len(dbDatabase) == 0 {
log.Fatalf("environment variable MYSQL_DATABASE is not set")
}
os.Setenv("MYSQL_HOST", dbHost) database := &models.Database{
os.Setenv("MYSQL_PORT", dbPort) Type: "mysql",
os.Setenv("MYSQL_USR", dbUser) MySQL: &models.MySQL{
os.Setenv("MYSQL_PWD", dbPassword) Host: dbHost,
Port: dbPort,
Username: dbUser,
Password: dbPassword,
Database: dbDatabase,
},
}
return testForAll(m) log.Infof("MYSQL_HOST: %s, MYSQL_USR: %s, MYSQL_PORT: %s, MYSQL_PWD: %s\n", dbHost, dbUser, dbPort, dbPassword)
return testForAll(m, database)
} }
func testForSQLite(m *testing.M) int { func testForSQLite(m *testing.M) int {
db := os.Getenv("DATABASE")
defer os.Setenv("DATABASE", db)
os.Setenv("DATABASE", "sqlite")
file := os.Getenv("SQLITE_FILE") file := os.Getenv("SQLITE_FILE")
if len(file) == 0 { if len(file) == 0 {
os.Setenv("SQLITE_FILE", "/registry.db") log.Fatalf("environment variable SQLITE_FILE is not set")
defer os.Setenv("SQLITE_FILE", "")
} }
return testForAll(m) database := &models.Database{
Type: "sqlite",
SQLite: &models.SQLite{
File: file,
},
}
return testForAll(m, database)
} }
func testForAll(m *testing.M) int { func testForAll(m *testing.M, database *models.Database) int {
os.Setenv("AUTH_MODE", "db_auth") initDatabaseForTest(database)
initDatabaseForTest()
clearUp(username) clearUp(username)
return m.Run() return m.Run()
@ -210,8 +222,8 @@ func testForAll(m *testing.M) int {
var defaultRegistered = false var defaultRegistered = false
func initDatabaseForTest() { func initDatabaseForTest(db *models.Database) {
database, err := getDatabase() database, err := getDatabase(db)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -226,6 +238,12 @@ func initDatabaseForTest() {
if err := database.Register(alias); err != nil { if err := database.Register(alias); err != nil {
panic(err) panic(err)
} }
if alias != "default" {
if err = globalOrm.Using(alias); err != nil {
log.Fatalf("failed to create new orm: %v", err)
}
}
} }
func TestRegister(t *testing.T) { func TestRegister(t *testing.T) {

View File

@ -16,15 +16,11 @@
package dao package dao
import ( import (
"errors"
"fmt" "fmt"
"net"
"time"
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
_ "github.com/go-sql-driver/mysql" //register mysql driver _ "github.com/go-sql-driver/mysql" //register mysql driver
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils"
) )
type mysql struct { type mysql struct {
@ -48,7 +44,8 @@ func NewMySQL(host, port, usr, pwd, database string) Database {
// Register registers MySQL as the underlying database used // Register registers MySQL as the underlying database used
func (m *mysql) Register(alias ...string) error { func (m *mysql) Register(alias ...string) error {
if err := m.testConn(m.host, m.port); err != nil {
if err := utils.TestTCPConn(m.host+":"+m.port, 60, 2); err != nil {
return err return err
} }
@ -65,30 +62,6 @@ func (m *mysql) Register(alias ...string) error {
return orm.RegisterDataBase(an, "mysql", conn) return orm.RegisterDataBase(an, "mysql", conn)
} }
func (m *mysql) testConn(host, port string) error {
ch := make(chan int, 1)
go func() {
var err error
var c net.Conn
for {
c, err = net.DialTimeout("tcp", host+":"+port, 20*time.Second)
if err == nil {
c.Close()
ch <- 1
} else {
log.Errorf("failed to connect to db, retry after 2 seconds :%v", err)
time.Sleep(2 * time.Second)
}
}
}()
select {
case <-ch:
return nil
case <-time.After(60 * time.Second):
return errors.New("failed to connect to database after 60 seconds")
}
}
// Name returns the name of MySQL // Name returns the name of MySQL
func (m *mysql) Name() string { func (m *mysql) Name() string {
return "MySQL" return "MySQL"

View File

@ -38,6 +38,11 @@ func TestDeleteUser(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("failed to register user: %v", err) t.Fatalf("failed to register user: %v", err)
} }
defer func(id int64) {
if err := deleteUser(id); err != nil {
t.Fatalf("failed to delete user %d: %v", id, err)
}
}(id)
err = DeleteUser(int(id)) err = DeleteUser(int(id))
if err != nil { if err != nil {
@ -67,3 +72,11 @@ func TestDeleteUser(t *testing.T) {
expected) expected)
} }
} }
func deleteUser(id int64) error {
if _, err := GetOrmer().QueryTable(&models.User{}).
Filter("UserID", id).Delete(); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,95 @@
/*
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package models
// Authentication ...
type Authentication struct {
Mode string `json:"mode"`
SelfRegistration bool `json:"self_registration"`
LDAP *LDAP `json:"ldap,omitempty"`
}
// LDAP ...
type LDAP struct {
URL string `json:"url"`
SearchDN string `json:"search_dn"`
SearchPwd string `json:"search_pwd"`
BaseDN string `json:"base_dn"`
Filter string `json:"filter"`
UID string `json:"uid"`
Scope int `json:"scope"`
}
// Database ...
type Database struct {
Type string `json:"type"`
MySQL *MySQL `json:"mysql,omitempty"`
SQLite *SQLite `json:"sqlite,omitempty"`
}
// MySQL ...
type MySQL struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Database string `json:"database"`
}
// SQLite ...
type SQLite struct {
File string `json:"file"`
}
// Email ...
type Email struct {
Host string `json:"host"`
Port string `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
TLS bool `json:"tls"`
Identity string `json:"identity"`
From string `json:"from"`
}
// Registry ...
type Registry struct {
URL string `json:"url"`
}
// TokenService ...
type TokenService struct {
URL string `json:"url"`
}
// SystemCfg holds all configurations of system
type SystemCfg struct {
DomainName string `json:"domain_name"` // Harbor external URL: protocal://host:port
Authentication *Authentication `json:"authentication"`
Database *Database `json:"database"`
TokenService *TokenService `json:"token_service"`
Registry *Registry `json:"registry"`
Email *Email `json:"email"`
VerifyRemoteCert bool `json:"verify_remote_cert"`
ProjectCreationRestriction string `json:"project_creation_restriction"`
MaxJobWorkers int `json:"max_job_workers"`
JobLogDir string `json:"job_log_dir"`
InitialAdminPwd string `json:"initial_admin_pwd"`
CompressJS bool `json:"compress_js"` //TODO remove
TokenExpiration int `json:"token_expiration"` // in minute
SecretKey string `json:"secret_key"`
CfgExpiration int `json:"cfg_expiration"`
}

View File

@ -13,17 +13,19 @@
limitations under the License. limitations under the License.
*/ */
package utils package email
import ( import (
"bytes" "bytes"
"crypto/tls" "crypto/tls"
"strings" //"strings"
"net/smtp" "net/smtp"
"text/template" "text/template"
"github.com/astaxie/beego" //"github.com/astaxie/beego"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/ui/config"
) )
// Mail holds information about content of Email // Mail holds information about content of Email
@ -34,24 +36,15 @@ type Mail struct {
Message string Message string
} }
// MailConfig holds information about Email configurations var mc models.Email
type MailConfig struct {
Identity string
Host string
Port string
Username string
Password string
TLS bool
}
var mc MailConfig
// SendMail sends Email according to the configurations // SendMail sends Email according to the configurations
func (m Mail) SendMail() error { func (m Mail) SendMail() error {
mc, err := config.Email()
if mc.Host == "" { if err != nil {
loadConfig() return err
} }
mailTemplate, err := template.ParseFiles("views/mail.tpl") mailTemplate, err := template.ParseFiles("views/mail.tpl")
if err != nil { if err != nil {
return err return err
@ -123,6 +116,7 @@ func sendMailWithTLS(m Mail, auth smtp.Auth, content []byte) error {
return client.Quit() return client.Quit()
} }
/*
func loadConfig() { func loadConfig() {
config, err := beego.AppConfig.GetSection("mail") config, err := beego.AppConfig.GetSection("mail")
if err != nil { if err != nil {
@ -142,3 +136,4 @@ func loadConfig() {
TLS: useTLS, TLS: useTLS,
} }
} }
*/

View File

@ -22,8 +22,6 @@ import (
"runtime" "runtime"
"sync" "sync"
"time" "time"
"github.com/vmware/harbor/src/common/config"
) )
var logger = New(os.Stdout, NewTextFormatter(), WarningLevel) var logger = New(os.Stdout, NewTextFormatter(), WarningLevel)
@ -31,7 +29,7 @@ var logger = New(os.Stdout, NewTextFormatter(), WarningLevel)
func init() { func init() {
logger.callDepth = 4 logger.callDepth = 4
lvl := config.LogLevel() lvl := os.Getenv("LOG_LEVEL")
if len(lvl) == 0 { if len(lvl) == 0 {
logger.SetLevel(InfoLevel) logger.SetLevel(InfoLevel)
return return

View File

@ -25,7 +25,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/vmware/harbor/src/common/config" //"github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils/registry" "github.com/vmware/harbor/src/common/utils/registry"
registry_error "github.com/vmware/harbor/src/common/utils/registry/error" registry_error "github.com/vmware/harbor/src/common/utils/registry/error"
@ -234,12 +234,15 @@ func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []
// 2. the realm field returned by registry is an IP which can not reachable // 2. the realm field returned by registry is an IP which can not reachable
// inside Harbor // inside Harbor
func tokenURL(realm string) string { func tokenURL(realm string) string {
extEndpoint := config.ExtEndpoint() //TODO
tokenEndpoint := config.TokenEndpoint() /*
if len(extEndpoint) != 0 && len(tokenEndpoint) != 0 && extEndpoint := config.ExtEndpoint()
strings.Contains(realm, extEndpoint) { tokenEndpoint := config.TokenEndpoint()
realm = strings.TrimRight(tokenEndpoint, "/") + "/service/token" if len(extEndpoint) != 0 && len(tokenEndpoint) != 0 &&
} strings.Contains(realm, extEndpoint) {
realm = strings.TrimRight(tokenEndpoint, "/") + "/service/token"
}
*/
return realm return realm
} }

View File

@ -16,10 +16,14 @@
package utils package utils
import ( import (
"fmt"
"math/rand" "math/rand"
"net"
"net/url" "net/url"
"strings" "strings"
"time" "time"
"github.com/vmware/harbor/src/common/utils/log"
) )
// FormatEndpoint formats endpoint // FormatEndpoint formats endpoint
@ -70,3 +74,37 @@ func GenerateRandomString() string {
} }
return string(result) return string(result)
} }
// timeout in second
func TestTCPConn(addr string, timeout, interval int) error {
success := make(chan int)
cancel := make(chan int)
go func() {
for {
select {
case <-cancel:
break
default:
conn, err := net.DialTimeout("tcp", addr, time.Duration(timeout)*time.Second)
if err != nil {
log.Errorf("failed to connect to tcp://%s, retry after %d seconds :%v",
addr, interval, err)
time.Sleep(time.Duration(interval) * time.Second)
continue
}
conn.Close()
success <- 1
break
}
}
}()
select {
case <-success:
return nil
case <-time.After(time.Duration(timeout) * time.Second):
cancel <- 1
return fmt.Errorf("failed to connect to tcp:%s after %d seconds", addr, timeout)
}
}

View File

@ -25,12 +25,12 @@ import (
"github.com/vmware/harbor/src/common/api" "github.com/vmware/harbor/src/common/api"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/jobservice/job"
"github.com/vmware/harbor/src/jobservice/config"
"github.com/vmware/harbor/src/jobservice/utils"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
u "github.com/vmware/harbor/src/common/utils" u "github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/jobservice/config"
"github.com/vmware/harbor/src/jobservice/job"
"github.com/vmware/harbor/src/jobservice/utils"
) )
// ReplicationJob handles /api/replicationJobs /api/replicationJobs/:id/log // ReplicationJob handles /api/replicationJobs /api/replicationJobs/:id/log
@ -171,7 +171,13 @@ func (rj *ReplicationJob) GetLog() {
rj.RenderError(http.StatusBadRequest, "Invalid job id") rj.RenderError(http.StatusBadRequest, "Invalid job id")
return return
} }
logFile := utils.GetJobLogPath(jid) logFile, err := utils.GetJobLogPath(jid)
if err != nil {
log.Errorf("failed to get log path of job %s: %v", idStr, err)
rj.RenderError(http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError))
return
}
rj.Ctx.Output.Download(logFile) rj.Ctx.Output.Download(logFile)
} }

View File

@ -16,121 +16,149 @@
package config package config
import ( import (
"fmt" "encoding/json"
"os" "os"
"strconv" "time"
"github.com/astaxie/beego" comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/models"
//"github.com/vmware/harbor/src/common/utils/log"
) )
const defaultMaxWorkers int = 10 var mg *comcfg.Manager
var maxJobWorkers int // Configuration holds configurations of Jobservice
var localUIURL string type Configuration struct {
var localRegURL string Database *models.Database `json:"database"`
var logDir string Registry *models.Registry `json:"registry"`
var uiSecret string VerifyRemoteCert bool `json:"verify_remote_cert"`
var secretKey string MaxJobWorkers int `json:"max_job_workers"`
var verifyRemoteCert string JobLogDir string `json:"job_log_dir"`
SecretKey string `json:"secret_key"`
CfgExpiration int `json:"cfg_expiration"`
}
func init() { func Init() error {
maxWorkersEnv := os.Getenv("MAX_JOB_WORKERS") adminServerURL := os.Getenv("ADMIN_SERVER_URL")
maxWorkers64, err := strconv.ParseInt(maxWorkersEnv, 10, 32) if len(adminServerURL) == 0 {
maxJobWorkers = int(maxWorkers64) adminServerURL = "http://admin_server"
}
mg = comcfg.NewManager("cfg", adminServerURL)
if err := mg.Loader.Init(); err != nil {
return err
}
if err := load(); err != nil {
return err
}
path, err := LogDir()
if err != nil { if err != nil {
log.Warningf("Failed to parse max works setting, error: %v, the default value: %d will be used", err, defaultMaxWorkers) return err
maxJobWorkers = defaultMaxWorkers }
if err := os.MkdirAll(path, 0600); err != nil {
return err
} }
localRegURL = os.Getenv("REGISTRY_URL") return nil
if len(localRegURL) == 0 { }
localRegURL = "http://registry:5000"
// get returns configurations of jobservice from cache,
// if cache is null, it loads first
func get() (*Configuration, error) {
cfg := mg.GetFromCache()
if cfg != nil {
return cfg.(*Configuration), nil
} }
localUIURL = os.Getenv("UI_URL") if err := load(); err != nil {
if len(localUIURL) == 0 { return nil, err
localUIURL = "http://ui"
} }
logDir = os.Getenv("LOG_DIR") return mg.GetFromCache().(*Configuration), nil
if len(logDir) == 0 { }
logDir = "/var/log"
}
f, err := os.Open(logDir) // load loads configurations of jobservice and puts them into cache
defer f.Close() func load() error {
raw, err := mg.Loader.Load()
if err != nil { if err != nil {
panic(err) return err
} }
finfo, err := f.Stat()
cfg := &Configuration{}
if err = json.Unmarshal(raw, cfg); err != nil {
return err
}
if err = mg.Cache.Put(mg.Key, cfg,
time.Duration(cfg.CfgExpiration)*time.Second); err != nil {
return err
}
return nil
}
// VerifyRemoteCert returns bool value.
func VerifyRemoteCert() (bool, error) {
cfg, err := get()
if err != nil { if err != nil {
panic(err) return true, err
}
if !finfo.IsDir() {
panic(fmt.Sprintf("%s is not a direcotry", logDir))
} }
return cfg.VerifyRemoteCert, nil
}
uiSecret = os.Getenv("UI_SECRET") // Database ...
if len(uiSecret) == 0 { func Database() (*models.Database, error) {
panic("UI Secret is not set") cfg, err := get()
if err != nil {
return nil, err
} }
return cfg.Database, nil
verifyRemoteCert = os.Getenv("VERIFY_REMOTE_CERT")
if len(verifyRemoteCert) == 0 {
verifyRemoteCert = "on"
}
configPath := os.Getenv("CONFIG_PATH")
if len(configPath) != 0 {
log.Infof("Config path: %s", configPath)
beego.LoadAppConfig("ini", configPath)
}
secretKey = os.Getenv("SECRET_KEY")
if len(secretKey) != 16 {
panic("The length of secretkey has to be 16 characters!")
}
log.Debugf("config: maxJobWorkers: %d", maxJobWorkers)
log.Debugf("config: localUIURL: %s", localUIURL)
log.Debugf("config: localRegURL: %s", localRegURL)
log.Debugf("config: verifyRemoteCert: %s", verifyRemoteCert)
log.Debugf("config: logDir: %s", logDir)
log.Debugf("config: uiSecret: ******")
} }
// MaxJobWorkers ... // MaxJobWorkers ...
func MaxJobWorkers() int { func MaxJobWorkers() (int, error) {
return maxJobWorkers cfg, err := get()
if err != nil {
return 0, err
}
return cfg.MaxJobWorkers, nil
} }
// LocalUIURL returns the local ui url, job service will use this URL to call API hosted on ui process // LocalUIURL returns the local ui url, job service will use this URL to call API hosted on ui process
func LocalUIURL() string { func LocalUIURL() string {
return localUIURL return "http://ui"
} }
// LocalRegURL returns the local registry url, job service will use this URL to pull image from the registry // LocalRegURL returns the local registry url, job service will use this URL to pull image from the registry
func LocalRegURL() string { func LocalRegURL() (string, error) {
return localRegURL cfg, err := get()
if err != nil {
return "", err
}
return cfg.Registry.URL, nil
} }
// LogDir returns the absolute path to which the log file will be written // LogDir returns the absolute path to which the log file will be written
func LogDir() string { func LogDir() (string, error) {
return logDir cfg, err := get()
if err != nil {
return "", err
}
return cfg.JobLogDir, nil
} }
// UISecret will return the value of secret cookie for jobsevice to call UI API. // UISecret will return the value of secret cookie for jobsevice to call UI API.
func UISecret() string { func UISecret() string {
return uiSecret return os.Getenv("UI_SECRET")
} }
// SecretKey will return the secret key for encryption/decryption password in target. // SecretKey will return the secret key for encryption/decryption password in target.
func SecretKey() string { func SecretKey() (string, error) {
return secretKey cfg, err := get()
} if err != nil {
return "", err
// VerifyRemoteCert return the flag to tell jobservice whether or not verify the cert of remote registry }
func VerifyRemoteCert() bool { return cfg.SecretKey, nil
return verifyRemoteCert != "off"
} }

View File

@ -20,12 +20,12 @@ import (
"sync" "sync"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/jobservice/config"
"github.com/vmware/harbor/src/jobservice/replication"
"github.com/vmware/harbor/src/jobservice/utils"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
uti "github.com/vmware/harbor/src/common/utils" uti "github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/jobservice/config"
"github.com/vmware/harbor/src/jobservice/replication"
"github.com/vmware/harbor/src/jobservice/utils"
) )
// RepJobParm wraps the parm of a job // RepJobParm wraps the parm of a job
@ -184,14 +184,17 @@ func (sm *SM) Init() {
} }
// Reset resets the state machine so it will start handling another job. // Reset resets the state machine so it will start handling another job.
func (sm *SM) Reset(jid int64) error { func (sm *SM) Reset(jid int64) (err error) {
//To ensure the new jobID is visible to the thread to stop the SM //To ensure the new jobID is visible to the thread to stop the SM
sm.lock.Lock() sm.lock.Lock()
sm.JobID = jid sm.JobID = jid
sm.desiredState = "" sm.desiredState = ""
sm.lock.Unlock() sm.lock.Unlock()
sm.Logger = utils.NewLogger(sm.JobID) sm.Logger, err = utils.NewLogger(sm.JobID)
if err != nil {
return
}
//init parms //init parms
job, err := dao.GetRepJob(sm.JobID) job, err := dao.GetRepJob(sm.JobID)
if err != nil { if err != nil {
@ -207,13 +210,22 @@ func (sm *SM) Reset(jid int64) error {
if policy == nil { if policy == nil {
return fmt.Errorf("The policy doesn't exist in DB, policy id:%d", job.PolicyID) return fmt.Errorf("The policy doesn't exist in DB, policy id:%d", job.PolicyID)
} }
regURL, err := config.LocalRegURL()
if err != nil {
return err
}
verify, err := config.VerifyRemoteCert()
if err != nil {
return err
}
sm.Parms = &RepJobParm{ sm.Parms = &RepJobParm{
LocalRegURL: config.LocalRegURL(), LocalRegURL: regURL,
Repository: job.Repository, Repository: job.Repository,
Tags: job.TagList, Tags: job.TagList,
Enabled: policy.Enabled, Enabled: policy.Enabled,
Operation: job.Operation, Operation: job.Operation,
Insecure: !config.VerifyRemoteCert(), Insecure: !verify,
} }
if policy.Enabled == 0 { if policy.Enabled == 0 {
//worker will cancel this job //worker will cancel this job
@ -231,7 +243,11 @@ func (sm *SM) Reset(jid int64) error {
pwd := target.Password pwd := target.Password
if len(pwd) != 0 { if len(pwd) != 0 {
pwd, err = uti.ReversibleDecrypt(pwd, config.SecretKey()) key, err := config.SecretKey()
if err != nil {
return err
}
pwd, err = uti.ReversibleDecrypt(pwd, key)
if err != nil { if err != nil {
return fmt.Errorf("failed to decrypt password: %v", err) return fmt.Errorf("failed to decrypt password: %v", err)
} }

View File

@ -17,9 +17,9 @@ package job
import ( import (
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/jobservice/config"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/jobservice/config"
) )
type workerPool struct { type workerPool struct {
@ -111,17 +111,22 @@ func NewWorker(id int) *Worker {
} }
// InitWorkerPool create workers according to configuration. // InitWorkerPool create workers according to configuration.
func InitWorkerPool() { func InitWorkerPool() error {
WorkerPool = &workerPool{ n, err := config.MaxJobWorkers()
workerChan: make(chan *Worker, config.MaxJobWorkers()), if err != nil {
workerList: make([]*Worker, 0, config.MaxJobWorkers()), return err
} }
for i := 0; i < config.MaxJobWorkers(); i++ { WorkerPool = &workerPool{
workerChan: make(chan *Worker, n),
workerList: make([]*Worker, 0, n),
}
for i := 0; i < n; i++ {
worker := NewWorker(i) worker := NewWorker(i)
WorkerPool.workerList = append(WorkerPool.workerList, worker) WorkerPool.workerList = append(WorkerPool.workerList, worker)
worker.Start() worker.Start()
log.Debugf("worker %d started", worker.ID) log.Debugf("worker %d started", worker.ID)
} }
return nil
} }
// Dispatch will listen to the jobQueue of job service and try to pick a free worker from the worker pool and assign the job to it. // Dispatch will listen to the jobQueue of job service and try to pick a free worker from the worker pool and assign the job to it.

View File

@ -18,13 +18,28 @@ package main
import ( import (
"github.com/astaxie/beego" "github.com/astaxie/beego"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/jobservice/job"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/jobservice/config"
"github.com/vmware/harbor/src/jobservice/job"
) )
func main() { func main() {
dao.InitDatabase() log.Info("initializing configurations...")
if err := config.Init(); err != nil {
log.Fatalf("failed to initialize configurations: %v", err)
}
log.Info("configurations initialization completed")
database, err := config.Database()
if err != nil {
log.Fatalf("failed to get database configurations: %v", err)
}
if err := dao.InitDatabase(database); err != nil {
log.Fatalf("failed to initialize database: %v", err)
}
initRouters() initRouters()
job.InitWorkerPool() job.InitWorkerPool()
go job.Dispatch() go job.Dispatch()
@ -48,3 +63,13 @@ func resumeJobs() {
log.Warningf("Failed to jobs to resume, error: %v", err) log.Warningf("Failed to jobs to resume, error: %v", err)
} }
} }
/*
func init() {
configPath := os.Getenv("CONFIG_PATH")
if len(configPath) != 0 {
log.Infof("Config path: %s", configPath)
beego.LoadAppConfig("ini", configPath)
}
}
*/

View File

@ -18,16 +18,20 @@ package utils
import ( import (
"fmt" "fmt"
"github.com/vmware/harbor/src/jobservice/config"
"github.com/vmware/harbor/src/common/utils/log"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/jobservice/config"
) )
// NewLogger create a logger for a speicified job // NewLogger create a logger for a speicified job
func NewLogger(jobID int64) *log.Logger { func NewLogger(jobID int64) (*log.Logger, error) {
logFile := GetJobLogPath(jobID) logFile, err := GetJobLogPath(jobID)
if err != nil {
return nil, err
}
d := filepath.Dir(logFile) d := filepath.Dir(logFile)
if _, err := os.Stat(d); os.IsNotExist(err) { if _, err := os.Stat(d); os.IsNotExist(err) {
err := os.MkdirAll(d, 0660) err := os.MkdirAll(d, 0660)
@ -40,11 +44,11 @@ func NewLogger(jobID int64) *log.Logger {
log.Errorf("Failed to open log file %s, the log of job %d will be printed to standard output, the error: %v", logFile, jobID, err) log.Errorf("Failed to open log file %s, the log of job %d will be printed to standard output, the error: %v", logFile, jobID, err)
f = os.Stdout f = os.Stdout
} }
return log.New(f, log.NewTextFormatter(), log.InfoLevel) return log.New(f, log.NewTextFormatter(), log.InfoLevel), nil
} }
// GetJobLogPath returns the absolute path in which the job log file is located. // GetJobLogPath returns the absolute path in which the job log file is located.
func GetJobLogPath(jobID int64) string { func GetJobLogPath(jobID int64) (string, error) {
f := fmt.Sprintf("job_%d.log", jobID) f := fmt.Sprintf("job_%d.log", jobID)
k := jobID / 1000 k := jobID / 1000
p := "" p := ""
@ -61,6 +65,10 @@ func GetJobLogPath(jobID int64) string {
p = filepath.Join(d, p) p = filepath.Join(d, p)
} }
p = filepath.Join(config.LogDir(), p, f) base, err := config.LogDir()
return p if err != nil {
return "", err
}
p = filepath.Join(base, p, f)
return p, nil
} }

245
src/ui/api/config.go Normal file
View File

@ -0,0 +1,245 @@
/*
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api
import (
"fmt"
"net/http"
"strconv"
//"strings"
"github.com/vmware/harbor/src/common/api"
comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/dao"
//"github.com/vmware/harbor/src/common/models"
//"github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
)
type ConfigAPI struct {
api.BaseAPI
}
// Prepare validates the user
func (c *ConfigAPI) Prepare() {
userID := c.ValidateUser()
isSysAdmin, err := dao.IsAdminRole(userID)
if err != nil {
log.Errorf("failed to check the role of user: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if !isSysAdmin {
c.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
}
}
// Get returns configurations
func (c *ConfigAPI) Get() {
cfg, err := config.GetSystemCfg()
if err != nil {
log.Errorf("failed to get configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
//TODO filter attr in sys config
c.Data["json"] = cfg
c.ServeJSON()
}
// Put updates configurations
func (c *ConfigAPI) Put() {
m := map[string]string{}
c.DecodeJSONReq(&m)
if err := validateCfg(m); err != nil {
c.CustomAbort(http.StatusBadRequest, err.Error())
}
if value, ok := m[comcfg.AUTH_MODE]; ok {
mode, err := config.AuthMode()
if err != nil {
log.Errorf("failed to get auth mode: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if mode != value {
flag, err := authModeCanBeModified()
if err != nil {
log.Errorf("failed to determine whether auth mode can be modified: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if !flag {
c.CustomAbort(http.StatusBadRequest,
fmt.Sprintf("%s can not be modified as new users have been inserted into database",
comcfg.AUTH_MODE))
}
}
}
log.Info(m)
if err := config.Upload(m); err != nil {
log.Errorf("failed to upload configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if err := config.Load(); err != nil {
log.Errorf("failed to load configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
}
// TODO ldap timeout, scope value
func validateCfg(c map[string]string) error {
if value, ok := c[comcfg.AUTH_MODE]; ok {
if value != comcfg.DB_AUTH && value != comcfg.LDAP_AUTH {
return fmt.Errorf("invalid %s, shoud be %s or %s", comcfg.AUTH_MODE, comcfg.DB_AUTH, comcfg.LDAP_AUTH)
}
if value == comcfg.LDAP_AUTH {
if _, ok := c[comcfg.LDAP_URL]; !ok {
return fmt.Errorf("%s is missing", comcfg.LDAP_URL)
}
if _, ok := c[comcfg.LDAP_BASE_DN]; !ok {
return fmt.Errorf("%s is missing", comcfg.LDAP_BASE_DN)
}
if _, ok := c[comcfg.LDAP_UID]; !ok {
return fmt.Errorf("%s is missing", comcfg.LDAP_UID)
}
if _, ok := c[comcfg.LDAP_SCOPE]; !ok {
return fmt.Errorf("%s is missing", comcfg.LDAP_SCOPE)
}
}
}
if ldapURL, ok := c[comcfg.LDAP_URL]; ok && len(ldapURL) == 0 {
return fmt.Errorf("%s is empty", comcfg.LDAP_URL)
}
if baseDN, ok := c[comcfg.LDAP_BASE_DN]; ok && len(baseDN) == 0 {
return fmt.Errorf("%s is empty", comcfg.LDAP_BASE_DN)
}
if uID, ok := c[comcfg.LDAP_UID]; ok && len(uID) == 0 {
return fmt.Errorf("%s is empty", comcfg.LDAP_UID)
}
if scope, ok := c[comcfg.LDAP_SCOPE]; ok &&
scope != comcfg.LDAP_SCOPE_BASE &&
scope != comcfg.LDAP_SCOPE_ONELEVEL &&
scope != comcfg.LDAP_SCOPE_SUBTREE {
return fmt.Errorf("invalid %s, should be %s, %s or %s",
comcfg.LDAP_SCOPE,
comcfg.LDAP_SCOPE_BASE,
comcfg.LDAP_SCOPE_ONELEVEL,
comcfg.LDAP_SCOPE_SUBTREE)
}
if self, ok := c[comcfg.SELF_REGISTRATION]; ok &&
self != "true" && self != "false" {
return fmt.Errorf("%s should be %s or %s",
comcfg.SELF_REGISTRATION, "true", "false")
}
if port, ok := c[comcfg.EMAIL_SERVER_PORT]; ok {
if p, err := strconv.Atoi(port); err != nil || p < 0 || p > 65535 {
return fmt.Errorf("invalid %s", comcfg.EMAIL_SERVER_PORT)
}
}
if ssl, ok := c[comcfg.EMAIL_SSL]; ok && ssl != "true" && ssl != "false" {
return fmt.Errorf("%s should be true or false", comcfg.EMAIL_SSL)
}
if crt, ok := c[comcfg.PROJECT_CREATION_RESTRICTION]; ok &&
crt != comcfg.PRO_CRT_RESTR_EVERYONE &&
crt != comcfg.PRO_CRT_RESTR_ADM_ONLY {
return fmt.Errorf("invalid %s, should be %s or %s",
comcfg.PROJECT_CREATION_RESTRICTION,
comcfg.PRO_CRT_RESTR_ADM_ONLY,
comcfg.PRO_CRT_RESTR_EVERYONE)
}
if verify, ok := c[comcfg.VERIFY_REMOTE_CERT]; ok && verify != "true" && verify != "false" {
return fmt.Errorf("invalid %s, should be true or false", comcfg.VERIFY_REMOTE_CERT)
}
if worker, ok := c[comcfg.MAX_JOB_WORKERS]; ok {
if w, err := strconv.Atoi(worker); err != nil || w <= 0 {
return fmt.Errorf("invalid %s", comcfg.MAX_JOB_WORKERS)
}
}
return nil
}
/*
func convert() ([]*models.Config, error) {
cfgs := []*models.Config{}
var err error
pwdKeys := []string{config.LDAP_SEARCH_PWD, config.EMAIL_PWD}
for _, pwdKey := range pwdKeys {
if pwd, ok := c[pwdKey]; ok && len(pwd) != 0 {
c[pwdKey], err = utils.ReversibleEncrypt(pwd, ui_cfg.SecretKey())
if err != nil {
return nil, err
}
}
}
for _, key := range configKeys {
if value, ok := c[key]; ok {
cfgs = append(cfgs, &models.Config{
Key: key,
Value: value,
})
}
}
return cfgs, nil
}
*/
/*
//[]*models.Config >> cfgForGet
func convert(cfg *config.Configuration) (map[string]interface{}, error) {
result := map[string]interface{}{}
for _, config := range configs {
cfg[config.Key] = &value{
Value: config.Value,
Editable: true,
}
}
dels := []string{config.LDAP_SEARCH_PWD, config.EMAIL_PWD}
for _, del := range dels {
if _, ok := cfg[del]; ok {
delete(cfg, del)
}
}
flag, err := authModeCanBeModified()
if err != nil {
return nil, err
}
cfg[config.AUTH_MODE].Editable = flag
return cfgForGet(cfg), nil
}
*/
func authModeCanBeModified() (bool, error) {
return dao.AuthModeCanBeModified()
}

View File

@ -77,7 +77,13 @@ func (p *ProjectAPI) Post() {
if err != nil { if err != nil {
log.Errorf("Failed to check admin role: %v", err) log.Errorf("Failed to check admin role: %v", err)
} }
if !isSysAdmin && config.OnlyAdminCreateProject() {
onlyAdmin, err := config.OnlyAdminCreateProject()
if err != nil {
log.Errorf("failed to determine whether only admin can create projects: %v", err)
p.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if !isSysAdmin && onlyAdmin {
log.Errorf("Only sys admin can create project") log.Errorf("Only sys admin can create project")
p.RenderError(http.StatusForbidden, "Only system admin can create project") p.RenderError(http.StatusForbidden, "Only system admin can create project")
return return

View File

@ -361,11 +361,19 @@ func (ra *RepositoryAPI) GetManifests() {
} }
func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repository, err error) { func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repository, err error) {
endpoint := config.InternalRegistryURL() endpoint, err := config.RegistryURL()
if err != nil {
return nil, err
}
insecure, err := api.GetIsInsecure()
if err != nil {
return nil, err
}
username, password, ok := ra.Ctx.Request.BasicAuth() username, password, ok := ra.Ctx.Request.BasicAuth()
if ok { if ok {
return newRepositoryClient(endpoint, api.GetIsInsecure(), username, password, return newRepositoryClient(endpoint, insecure, username, password,
repoName, "repository", repoName, "pull", "push", "*") repoName, "repository", repoName, "pull", "push", "*")
} }
@ -374,7 +382,7 @@ func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repo
return nil, err return nil, err
} }
return cache.NewRepositoryClient(endpoint, api.GetIsInsecure(), username, repoName, return cache.NewRepositoryClient(endpoint, insecure, username, repoName,
"repository", repoName, "pull", "push", "*") "repository", repoName, "pull", "push", "*")
} }

View File

@ -41,7 +41,12 @@ type TargetAPI struct {
// Prepare validates the user // Prepare validates the user
func (t *TargetAPI) Prepare() { func (t *TargetAPI) Prepare() {
t.secretKey = config.SecretKey() var err error
t.secretKey, err = config.SecretKey()
if err != nil {
log.Errorf("failed to get secret key: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
userID := t.ValidateUser() userID := t.ValidateUser()
isSysAdmin, err := dao.IsAdminRole(userID) isSysAdmin, err := dao.IsAdminRole(userID)
@ -97,7 +102,12 @@ func (t *TargetAPI) Ping() {
password = t.GetString("password") password = t.GetString("password")
} }
registry, err := newRegistryClient(endpoint, api.GetIsInsecure(), username, password, insecure, err := api.GetIsInsecure()
if err != nil {
log.Errorf("failed to check whether insecure or not: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
registry, err := newRegistryClient(endpoint, insecure, username, password,
"", "", "") "", "", "")
if err != nil { if err != nil {
// timeout, dns resolve error, connection refused, etc. // timeout, dns resolve error, connection refused, etc.

View File

@ -46,10 +46,21 @@ type passwordReq struct {
// Prepare validates the URL and parms // Prepare validates the URL and parms
func (ua *UserAPI) Prepare() { func (ua *UserAPI) Prepare() {
mode, err := config.AuthMode()
if err != nil {
log.Errorf("failed to get auth mode: %v", err)
ua.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
ua.AuthMode = config.AuthMode() ua.AuthMode = mode
ua.SelfRegistration = config.SelfRegistration() self, err := config.SelfRegistration()
if err != nil {
log.Errorf("failed to get self registration: %v", err)
ua.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
ua.SelfRegistration = self
if ua.Ctx.Input.IsPost() { if ua.Ctx.Input.IsPost() {
sessionUserID := ua.GetSession("userId") sessionUserID := ua.GetSession("userId")
@ -82,7 +93,6 @@ func (ua *UserAPI) Prepare() {
} }
} }
var err error
ua.IsAdmin, err = dao.IsAdminRole(ua.currentUserID) ua.IsAdmin, err = dao.IsAdminRole(ua.currentUserID)
if err != nil { if err != nil {
log.Errorf("Error occurred in IsAdminRole:%v", err) log.Errorf("Error occurred in IsAdminRole:%v", err)
@ -234,7 +244,7 @@ func (ua *UserAPI) Delete() {
return return
} }
if config.AuthMode() == "ldap_auth" { if ua.AuthMode == "ldap_auth" {
ua.CustomAbort(http.StatusForbidden, "user can not be deleted in LDAP authentication mode") ua.CustomAbort(http.StatusForbidden, "user can not be deleted in LDAP authentication mode")
} }

View File

@ -20,11 +20,9 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net"
"net/http" "net/http"
"sort" "sort"
"strings" "strings"
"time"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
@ -242,7 +240,7 @@ func addAuthentication(req *http.Request) {
// SyncRegistry syncs the repositories of registry with database. // SyncRegistry syncs the repositories of registry with database.
func SyncRegistry() error { func SyncRegistry() error {
log.Debugf("Start syncing repositories from registry to DB... ") log.Infof("Start syncing repositories from registry to DB... ")
reposInRegistry, err := catalog() reposInRegistry, err := catalog()
if err != nil { if err != nil {
@ -304,7 +302,7 @@ func SyncRegistry() error {
} }
} }
log.Debugf("Sync repositories from registry to DB is done.") log.Infof("Sync repositories from registry to DB is done.")
return nil return nil
} }
@ -350,7 +348,10 @@ func diffRepos(reposInRegistry []string, reposInDB []string) ([]string, []string
} }
// TODO remove the workaround when the bug of registry is fixed // TODO remove the workaround when the bug of registry is fixed
endpoint := config.InternalRegistryURL() endpoint, err := config.RegistryURL()
if err != nil {
return needsAdd, needsDel, err
}
client, err := cache.NewRepositoryClient(endpoint, true, client, err := cache.NewRepositoryClient(endpoint, true,
"admin", repoInR, "repository", repoInR) "admin", repoInR, "repository", repoInR)
if err != nil { if err != nil {
@ -372,7 +373,10 @@ func diffRepos(reposInRegistry []string, reposInDB []string) ([]string, []string
j++ j++
} else { } else {
// TODO remove the workaround when the bug of registry is fixed // TODO remove the workaround when the bug of registry is fixed
endpoint := config.InternalRegistryURL() endpoint, err := config.RegistryURL()
if err != nil {
return needsAdd, needsDel, err
}
client, err := cache.NewRepositoryClient(endpoint, true, client, err := cache.NewRepositoryClient(endpoint, true,
"admin", repoInR, "repository", repoInR) "admin", repoInR, "repository", repoInR)
if err != nil { if err != nil {
@ -422,32 +426,18 @@ func projectExists(repository string) (bool, error) {
} }
func initRegistryClient() (r *registry.Registry, err error) { func initRegistryClient() (r *registry.Registry, err error) {
endpoint := config.InternalRegistryURL() endpoint, err := config.RegistryURL()
if err != nil {
addr := endpoint return nil, err
if strings.Contains(endpoint, "/") {
addr = endpoint[strings.LastIndex(endpoint, "/")+1:]
} }
ch := make(chan int, 1) addr := endpoint
go func() { if strings.Contains(endpoint, "://") {
var err error addr = strings.Split(endpoint, "://")[1]
var c net.Conn }
for {
c, err = net.DialTimeout("tcp", addr, 20*time.Second) if err := utils.TestTCPConn(addr, 60, 2); err != nil {
if err == nil { return nil, err
c.Close()
ch <- 1
} else {
log.Errorf("failed to connect to registry client, retry after 2 seconds :%v", err)
time.Sleep(2 * time.Second)
}
}
}()
select {
case <-ch:
case <-time.After(60 * time.Second):
panic("Failed to connect to registry client after 60 seconds")
} }
registryClient, err := cache.NewRegistryClient(endpoint, true, "admin", registryClient, err := cache.NewRegistryClient(endpoint, true, "admin",

View File

@ -50,7 +50,10 @@ func Register(name string, authenticator Authenticator) {
// Login authenticates user credentials based on setting. // Login authenticates user credentials based on setting.
func Login(m models.AuthModel) (*models.User, error) { func Login(m models.AuthModel) (*models.User, error) {
var authMode = config.AuthMode() authMode, err := config.AuthMode()
if err != nil {
return nil, err
}
if authMode == "" || m.Principal == "admin" { if authMode == "" || m.Principal == "admin" {
authMode = "db_auth" authMode = "db_auth"
} }

View File

@ -46,7 +46,13 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
return nil, fmt.Errorf("the principal contains meta char: %q", c) return nil, fmt.Errorf("the principal contains meta char: %q", c)
} }
} }
ldapURL := config.LDAP().URL
settings, err := config.LDAP()
if err != nil {
return nil, err
}
ldapURL := settings.URL
if ldapURL == "" { if ldapURL == "" {
return nil, errors.New("can not get any available LDAP_URL") return nil, errors.New("can not get any available LDAP_URL")
} }
@ -57,16 +63,16 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
} }
ldap.SetOption(openldap.LDAP_OPT_PROTOCOL_VERSION, openldap.LDAP_VERSION3) ldap.SetOption(openldap.LDAP_OPT_PROTOCOL_VERSION, openldap.LDAP_VERSION3)
ldapBaseDn := config.LDAP().BaseDn ldapBaseDn := settings.BaseDN
if ldapBaseDn == "" { if ldapBaseDn == "" {
return nil, errors.New("can not get any available LDAP_BASE_DN") return nil, errors.New("can not get any available LDAP_BASE_DN")
} }
log.Debug("baseDn:", ldapBaseDn) log.Debug("baseDn:", ldapBaseDn)
ldapSearchDn := config.LDAP().SearchDn ldapSearchDn := settings.SearchDN
if ldapSearchDn != "" { if ldapSearchDn != "" {
log.Debug("Search DN: ", ldapSearchDn) log.Debug("Search DN: ", ldapSearchDn)
ldapSearchPwd := config.LDAP().SearchPwd ldapSearchPwd := settings.SearchPwd
err = ldap.Bind(ldapSearchDn, ldapSearchPwd) err = ldap.Bind(ldapSearchDn, ldapSearchPwd)
if err != nil { if err != nil {
log.Debug("Bind search dn error", err) log.Debug("Bind search dn error", err)
@ -74,8 +80,8 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
} }
} }
attrName := config.LDAP().UID attrName := settings.UID
filter := config.LDAP().Filter filter := settings.Filter
if filter != "" { if filter != "" {
filter = "(&" + filter + "(" + attrName + "=" + m.Principal + "))" filter = "(&" + filter + "(" + attrName + "=" + m.Principal + "))"
} else { } else {
@ -83,11 +89,11 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
} }
log.Debug("one or more filter", filter) log.Debug("one or more filter", filter)
ldapScope := config.LDAP().Scope ldapScope := settings.Scope
var scope int var scope int
if ldapScope == "1" { if ldapScope == 1 {
scope = openldap.LDAP_SCOPE_BASE scope = openldap.LDAP_SCOPE_BASE
} else if ldapScope == "2" { } else if ldapScope == 2 {
scope = openldap.LDAP_SCOPE_ONELEVEL scope = openldap.LDAP_SCOPE_ONELEVEL
} else { } else {
scope = openldap.LDAP_SCOPE_SUBTREE scope = openldap.LDAP_SCOPE_SUBTREE

View File

@ -13,143 +13,225 @@
limitations under the License. limitations under the License.
*/ */
// Package config provides methods to get configurations required by code in src/ui
package config package config
import ( import (
"strconv" "encoding/json"
"strings" "os"
"time"
commonConfig "github.com/vmware/harbor/src/common/config" comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/models"
) )
// LDAPSetting wraps the setting of an LDAP server var mg *comcfg.Manager
type LDAPSetting struct {
URL string type Configuration struct {
BaseDn string DomainName string `json:"domain_name"` // Harbor external URL: protocal://host:port
SearchDn string Authentication *models.Authentication `json:"authentication"`
SearchPwd string Database *models.Database `json:"database"`
UID string TokenService *models.TokenService `json:"token_service"`
Filter string Registry *models.Registry `json:"registry"`
Scope string Email *models.Email `json:"email"`
VerifyRemoteCert bool `json:"verify_remote_cert"`
ProjectCreationRestriction string `json:"project_creation_restriction"`
InitialAdminPwd string `json:"initial_admin_pwd"`
//TODO remove
CompressJS bool `json:"compress_js"`
TokenExpiration int `json:"token_expiration"`
SecretKey string `json:"secret_key"`
CfgExpiration int `json:"cfg_expiration`
} }
type uiParser struct{} func Init() error {
adminServerURL := os.Getenv("ADMIN_SERVER_URL")
if len(adminServerURL) == 0 {
adminServerURL = "http://admin_server"
}
mg = comcfg.NewManager("cfg", adminServerURL)
// Parse parses the auth settings url settings and other configuration consumed by code under src/ui if err := mg.Loader.Init(); err != nil {
func (up *uiParser) Parse(raw map[string]string, config map[string]interface{}) error { return err
mode := raw["AUTH_MODE"]
if mode == "ldap_auth" {
setting := LDAPSetting{
URL: raw["LDAP_URL"],
BaseDn: raw["LDAP_BASE_DN"],
SearchDn: raw["LDAP_SEARCH_DN"],
SearchPwd: raw["LDAP_SEARCH_PWD"],
UID: raw["LDAP_UID"],
Filter: raw["LDAP_FILTER"],
Scope: raw["LDAP_SCOPE"],
}
config["ldap"] = setting
} }
config["auth_mode"] = mode
var tokenExpiration = 30 //minutes if err := Load(); err != nil {
if len(raw["TOKEN_EXPIRATION"]) > 0 { return err
i, err := strconv.Atoi(raw["TOKEN_EXPIRATION"])
if err != nil {
log.Warningf("failed to parse token expiration: %v, using default value %d", err, tokenExpiration)
} else if i <= 0 {
log.Warningf("invalid token expiration, using default value: %d minutes", tokenExpiration)
} else {
tokenExpiration = i
}
} }
config["token_exp"] = tokenExpiration
config["admin_password"] = raw["HARBOR_ADMIN_PASSWORD"]
config["ext_reg_url"] = raw["EXT_REG_URL"]
config["ui_secret"] = raw["UI_SECRET"]
config["secret_key"] = raw["SECRET_KEY"]
config["self_registration"] = raw["SELF_REGISTRATION"] != "off"
config["admin_create_project"] = strings.ToLower(raw["PROJECT_CREATION_RESTRICTION"]) == "adminonly"
registryURL := raw["REGISTRY_URL"]
registryURL = strings.TrimRight(registryURL, "/")
config["internal_registry_url"] = registryURL
jobserviceURL := raw["JOB_SERVICE_URL"]
jobserviceURL = strings.TrimRight(jobserviceURL, "/")
config["internal_jobservice_url"] = jobserviceURL
return nil return nil
} }
var uiConfig *commonConfig.Config // Get returns configurations of UI, if cache is null, it loads first
func get() (*Configuration, error) {
cfg := mg.GetFromCache()
if cfg != nil {
return cfg.(*Configuration), nil
}
func init() { if err := Load(); err != nil {
uiKeys := []string{"AUTH_MODE", "LDAP_URL", "LDAP_BASE_DN", "LDAP_SEARCH_DN", "LDAP_SEARCH_PWD", "LDAP_UID", "LDAP_FILTER", "LDAP_SCOPE", "TOKEN_EXPIRATION", "HARBOR_ADMIN_PASSWORD", "EXT_REG_URL", "UI_SECRET", "SECRET_KEY", "SELF_REGISTRATION", "PROJECT_CREATION_RESTRICTION", "REGISTRY_URL", "JOB_SERVICE_URL"} return nil, err
uiConfig = &commonConfig.Config{
Config: make(map[string]interface{}),
Loader: &commonConfig.EnvConfigLoader{Keys: uiKeys},
Parser: &uiParser{},
}
if err := uiConfig.Load(); err != nil {
panic(err)
} }
return mg.GetFromCache().(*Configuration), nil
} }
// Reload ... // Load loads configurations of UI and puts them into cache
func Reload() error { func Load() error {
return uiConfig.Load() raw, err := mg.Loader.Load()
if err != nil {
return err
}
cfg := &Configuration{}
if err = json.Unmarshal(raw, cfg); err != nil {
return err
}
if err = mg.Cache.Put(mg.Key, cfg,
time.Duration(cfg.CfgExpiration)*time.Second); err != nil {
return err
}
return nil
}
// Upload uploads all system configutations to admin server
func Upload(cfg map[string]string) error {
b, err := json.Marshal(cfg)
if err != nil {
return err
}
return mg.Loader.Upload(b)
}
// GetSystemCfg returns the system configurations
func GetSystemCfg() (*models.SystemCfg, error) {
raw, err := mg.Loader.Load()
if err != nil {
return nil, err
}
cfg := &models.SystemCfg{}
if err = json.Unmarshal(raw, cfg); err != nil {
return nil, err
}
return cfg, nil
} }
// AuthMode ... // AuthMode ...
func AuthMode() string { func AuthMode() (string, error) {
return uiConfig.Config["auth_mode"].(string) cfg, err := get()
if err != nil {
return "", err
}
return cfg.Authentication.Mode, nil
} }
// LDAP returns the setting of ldap server // LDAP returns the setting of ldap server
func LDAP() LDAPSetting { func LDAP() (*models.LDAP, error) {
return uiConfig.Config["ldap"].(LDAPSetting) cfg, err := get()
if err != nil {
return nil, err
}
return cfg.Authentication.LDAP, nil
} }
// TokenExpiration returns the token expiration time (in minute) // TokenExpiration returns the token expiration time (in minute)
func TokenExpiration() int { func TokenExpiration() (int, error) {
return uiConfig.Config["token_exp"].(int) cfg, err := get()
if err != nil {
return 0, err
}
return cfg.TokenExpiration, nil
} }
// ExtRegistryURL returns the registry URL to exposed to external client // DomainName returns the external URL of Harbor: protocal://host:port
func ExtRegistryURL() string { func DomainName() (string, error) {
return uiConfig.Config["ext_reg_url"].(string) cfg, err := get()
} if err != nil {
return "", err
// UISecret returns the value of UI secret cookie, used for communication between UI and JobService }
func UISecret() string { return cfg.DomainName, nil
return uiConfig.Config["ui_secret"].(string)
} }
// SecretKey returns the secret key to encrypt the password of target // SecretKey returns the secret key to encrypt the password of target
func SecretKey() string { func SecretKey() (string, error) {
return uiConfig.Config["secret_key"].(string) cfg, err := get()
if err != nil {
return "", err
}
return cfg.SecretKey, nil
} }
// SelfRegistration returns the enablement of self registration // SelfRegistration returns the enablement of self registration
func SelfRegistration() bool { func SelfRegistration() (bool, error) {
return uiConfig.Config["self_registration"].(bool) cfg, err := get()
if err != nil {
return false, err
}
return cfg.Authentication.SelfRegistration, nil
} }
// InternalRegistryURL returns registry URL for internal communication between Harbor containers // RegistryURL ...
func InternalRegistryURL() string { func RegistryURL() (string, error) {
return uiConfig.Config["internal_registry_url"].(string) cfg, err := get()
if err != nil {
return "", err
}
return cfg.Registry.URL, nil
} }
// InternalJobServiceURL returns jobservice URL for internal communication between Harbor containers // InternalJobServiceURL returns jobservice URL for internal communication between Harbor containers
func InternalJobServiceURL() string { func InternalJobServiceURL() string {
return uiConfig.Config["internal_jobservice_url"].(string) return "http://jobservice"
} }
// InitialAdminPassword returns the initial password for administrator // InitialAdminPassword returns the initial password for administrator
func InitialAdminPassword() string { func InitialAdminPassword() (string, error) {
return uiConfig.Config["admin_password"].(string) cfg, err := get()
if err != nil {
return "", err
}
return cfg.InitialAdminPwd, nil
} }
// TODO
// OnlyAdminCreateProject returns the flag to restrict that only sys admin can create project // OnlyAdminCreateProject returns the flag to restrict that only sys admin can create project
func OnlyAdminCreateProject() bool { func OnlyAdminCreateProject() (bool, error) {
return uiConfig.Config["admin_create_project"].(bool) cfg, err := get()
if err != nil {
return true, err
}
return cfg.ProjectCreationRestriction == comcfg.PRO_CRT_RESTR_ADM_ONLY, nil
}
// VerifyRemoteCert returns bool value.
func VerifyRemoteCert() (bool, error) {
cfg, err := get()
if err != nil {
return true, err
}
return cfg.VerifyRemoteCert, nil
}
func Email() (*models.Email, error) {
cfg, err := get()
if err != nil {
return nil, err
}
return cfg.Email, nil
}
func Database() (*models.Database, error) {
cfg, err := get()
if err != nil {
return nil, err
}
return cfg.Database, nil
}
// TODO
// UISecret returns the value of UI secret cookie, used for communication between UI and JobService
func UISecret() string {
return os.Getenv("UI_SECRET")
} }

View File

@ -103,7 +103,12 @@ func (b *BaseController) Prepare() {
b.Data["CurLang"] = curLang.Name b.Data["CurLang"] = curLang.Name
b.Data["RestLangs"] = restLangs b.Data["RestLangs"] = restLangs
authMode := config.AuthMode() authMode, err := config.AuthMode()
if err != nil {
log.Errorf("failed to get auth mode: %v", err)
b.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if authMode == "" { if authMode == "" {
authMode = "db_auth" authMode = "db_auth"
} }
@ -120,9 +125,13 @@ func (b *BaseController) Prepare() {
b.UseCompressedJS = false b.UseCompressedJS = false
} }
b.SelfRegistration = config.SelfRegistration() b.SelfRegistration, err = config.SelfRegistration()
if err != nil {
log.Errorf("failed to get self registration: %v", err)
b.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
b.Data["SelfRegistration"] = config.SelfRegistration() b.Data["SelfRegistration"] = b.SelfRegistration
sessionUserID := b.GetSession("userId") sessionUserID := b.GetSession("userId")
if sessionUserID != nil { if sessionUserID != nil {

View File

@ -6,12 +6,12 @@ import (
"regexp" "regexp"
"text/template" "text/template"
"github.com/astaxie/beego"
"github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils" "github.com/vmware/harbor/src/common/utils"
email_util "github.com/vmware/harbor/src/common/utils/email"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
) )
type messageDetail struct { type messageDetail struct {
@ -49,7 +49,11 @@ func (cc *CommonController) SendEmail() {
message := new(bytes.Buffer) message := new(bytes.Buffer)
harborURL := config.ExtEndpoint() harborURL, err := config.DomainName()
if err != nil {
log.Errorf("failed to get domain name: %v", err)
cc.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if harborURL == "" { if harborURL == "" {
harborURL = "localhost" harborURL = "localhost"
} }
@ -65,14 +69,14 @@ func (cc *CommonController) SendEmail() {
cc.CustomAbort(http.StatusInternalServerError, "internal_error") cc.CustomAbort(http.StatusInternalServerError, "internal_error")
} }
config, err := beego.AppConfig.GetSection("mail") emailSettings, err := config.Email()
if err != nil { if err != nil {
log.Errorf("Can not load app.conf: %v", err) log.Errorf("failed to get email configurations: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "internal_error") cc.CustomAbort(http.StatusInternalServerError, "internal_error")
} }
mail := utils.Mail{ mail := email_util.Mail{
From: config["from"], From: emailSettings.From,
To: []string{email}, To: []string{email},
Subject: cc.Tr("reset_email_subject"), Subject: cc.Tr("reset_email_subject"),
Message: message.String()} Message: message.String()}

View File

@ -1,6 +1,8 @@
package controllers package controllers
import ( import (
"net/http"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config" "github.com/vmware/harbor/src/ui/config"
@ -23,6 +25,11 @@ func (pc *ProjectController) Get() {
isSysAdmin = false isSysAdmin = false
} }
} }
pc.Data["CanCreate"] = !config.OnlyAdminCreateProject() || isSysAdmin onlyAdmin, err := config.OnlyAdminCreateProject()
if err != nil {
log.Errorf("failed to determine whether only admin can create projects: %v", err)
pc.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
pc.Data["CanCreate"] = !onlyAdmin || isSysAdmin
pc.Forward("page_title_project", "project.htm") pc.Forward("page_title_project", "project.htm")
} }

View File

@ -1,6 +1,10 @@
package controllers package controllers
import ( import (
"net/http"
"strings"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config" "github.com/vmware/harbor/src/ui/config"
) )
@ -11,6 +15,11 @@ type RepositoryController struct {
// Get renders repository page // Get renders repository page
func (rc *RepositoryController) Get() { func (rc *RepositoryController) Get() {
rc.Data["HarborRegUrl"] = config.ExtRegistryURL() url, err := config.DomainName()
if err != nil {
log.Errorf("failed to get domain name: %v", err)
rc.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
rc.Data["HarborRegUrl"] = strings.Split(url, "://")[1]
rc.Forward("page_title_repository", "repository.htm") rc.Forward("page_title_repository", "repository.htm")
} }

View File

@ -64,7 +64,6 @@ func updateInitPassword(userID int, password string) error {
} }
func main() { func main() {
beego.BConfig.WebConfig.Session.SessionOn = true beego.BConfig.WebConfig.Session.SessionOn = true
//TODO //TODO
redisURL := os.Getenv("_REDIS_URL") redisURL := os.Getenv("_REDIS_URL")
@ -72,12 +71,28 @@ func main() {
beego.BConfig.WebConfig.Session.SessionProvider = "redis" beego.BConfig.WebConfig.Session.SessionProvider = "redis"
beego.BConfig.WebConfig.Session.SessionProviderConfig = redisURL beego.BConfig.WebConfig.Session.SessionProviderConfig = redisURL
} }
//
beego.AddTemplateExt("htm") beego.AddTemplateExt("htm")
dao.InitDatabase() log.Info("initializing configurations...")
if err := config.Init(); err != nil {
log.Fatalf("failed to initialize configurations: %v", err)
}
log.Info("configurations initialization completed")
if err := updateInitPassword(adminUserID, config.InitialAdminPassword()); err != nil { database, err := config.Database()
if err != nil {
log.Fatalf("failed to get database configuration: %v", err)
}
if err := dao.InitDatabase(database); err != nil {
log.Fatalf("failed to initialize database: %v", err)
}
password, err := config.InitialAdminPassword()
if err != nil {
log.Fatalf("failed to get admin's initia password: %v", err)
}
if err := updateInitPassword(adminUserID, password); err != nil {
log.Error(err) log.Error(err)
} }
initRouters() initRouters()

View File

@ -84,6 +84,7 @@ func initRouters() {
beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole") beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole")
beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos") beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos")
beego.Router("/api/logs", &api.LogAPI{}) beego.Router("/api/logs", &api.LogAPI{})
beego.Router("/api/configurations", &api.ConfigAPI{})
beego.Router("/api/systeminfo/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo") beego.Router("/api/systeminfo/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo")
beego.Router("/api/systeminfo/getcert", &api.SystemInfoAPI{}, "get:GetCert") beego.Router("/api/systeminfo/getcert", &api.SystemInfoAPI{}, "get:GetCert")

View File

@ -37,13 +37,6 @@ const (
privateKey = "/etc/ui/private_key.pem" privateKey = "/etc/ui/private_key.pem"
) )
var expiration int //minutes
func init() {
expiration = config.TokenExpiration()
log.Infof("token expiration: %d minutes", expiration)
}
// GetResourceActions ... // GetResourceActions ...
func GetResourceActions(scopes []string) []*token.ResourceActions { func GetResourceActions(scopes []string) []*token.ResourceActions {
log.Debugf("scopes: %+v", scopes) log.Debugf("scopes: %+v", scopes)
@ -91,7 +84,12 @@ func FilterAccess(username string, a *token.ResourceActions) {
repoLength := len(repoSplit) repoLength := len(repoSplit)
if repoLength > 1 { //Only check the permission when the requested image has a namespace, i.e. project if repoLength > 1 { //Only check the permission when the requested image has a namespace, i.e. project
var projectName string var projectName string
registryURL := config.ExtRegistryURL() registryURL, err := config.DomainName()
if err != nil {
log.Errorf("failed to get domain name: %v", err)
return
}
registryURL = strings.Split(registryURL, "://")[1]
if repoSplit[0] == registryURL { if repoSplit[0] == registryURL {
projectName = repoSplit[1] projectName = repoSplit[1]
log.Infof("Detected Registry URL in Project Name. Assuming this is a notary request and setting Project Name as %s\n", projectName) log.Infof("Detected Registry URL in Project Name. Assuming this is a notary request and setting Project Name as %s\n", projectName)
@ -153,6 +151,11 @@ func MakeToken(username, service string, access []*token.ResourceActions) (token
if err != nil { if err != nil {
return "", 0, nil, err return "", 0, nil, err
} }
expiration, err := config.TokenExpiration()
if err != nil {
return "", 0, nil, err
}
tk, expiresIn, issuedAt, err := makeTokenCore(issuer, username, service, expiration, access, pk) tk, expiresIn, issuedAt, err := makeTokenCore(issuer, username, service, expiration, access, pk)
if err != nil { if err != nil {
return "", 0, nil, err return "", 0, nil, err