diff --git a/src/common/config/manager.go b/src/common/config/manager.go new file mode 100644 index 000000000..d895e3dbd --- /dev/null +++ b/src/common/config/manager.go @@ -0,0 +1,181 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "fmt" + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/common/config/metadata" + "github.com/goharbor/harbor/src/common/config/store" + "github.com/goharbor/harbor/src/common/config/store/driver" + "github.com/goharbor/harbor/src/common/http/modifier/auth" + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/utils/log" + "os" +) + +// CfgManager ... Configure Manager +type CfgManager struct { + store *store.ConfigStore +} + +// NewDBCfgManager - create DB config manager +func NewDBCfgManager() *CfgManager { + manager := &CfgManager{store: store.NewConfigStore(&driver.Database{})} + // load default value + manager.loadDefault() + // load system config from env + manager.loadSystemConfigFromEnv() + return manager +} + +// NewRESTCfgManager - create REST config manager +func NewRESTCfgManager(configURL, secret string) *CfgManager { + secAuth := auth.NewSecretAuthorizer(secret) + manager := &CfgManager{store: store.NewConfigStore(driver.NewRESTDriver(configURL, secAuth))} + return manager +} + +// InmemoryDriver driver for unit testing +type InmemoryDriver struct { + cfgMap map[string]interface{} +} + +// Load ... +func (d *InmemoryDriver) Load() (map[string]interface{}, error) { + return d.cfgMap, nil +} + +// Save ... +func (d *InmemoryDriver) Save(cfg map[string]interface{}) error { + for k, v := range cfg { + d.cfgMap[k] = v + } + return nil +} + +// NewInMemoryManager create a manager for unit testing, doesn't involve database or REST +func NewInMemoryManager() *CfgManager { + return &CfgManager{store: store.NewConfigStore(&InmemoryDriver{cfgMap: map[string]interface{}{}})} +} + +// loadDefault ... +func (c *CfgManager) loadDefault() { + // Init Default Value + itemArray := metadata.Instance().GetAll() + for _, item := range itemArray { + // Every string type have default value, other types should have a default value + if _, ok := item.ItemType.(*metadata.StringType); ok || len(item.DefaultValue) > 0 { + cfgValue, err := metadata.NewCfgValue(item.Name, item.DefaultValue) + if err != nil { + log.Errorf("loadDefault failed, config item, key: %v, err: %v", item.Name, err) + continue + } + c.store.Set(item.Name, *cfgValue) + } + } +} + +// loadSystemConfigFromEnv ... +func (c *CfgManager) loadSystemConfigFromEnv() { + itemArray := metadata.Instance().GetAll() + // Init System Value + for _, item := range itemArray { + if item.Scope == metadata.SystemScope && len(item.EnvKey) > 0 { + if envValue, ok := os.LookupEnv(item.EnvKey); ok { + configValue, err := metadata.NewCfgValue(item.Name, envValue) + if err != nil { + log.Errorf("loadSystemConfigFromEnv failed, config item, key: %v, err: %v", item.Name, err) + continue + } + c.store.Set(item.Name, *configValue) + } + } + } +} + +// GetAll ... Get all settings +func (c *CfgManager) GetAll() []metadata.ConfigureValue { + results := make([]metadata.ConfigureValue, 0) + if err := c.store.Load(); err != nil { + log.Errorf("GetAll failed, error %v", err) + return results + } + metaDataList := metadata.Instance().GetAll() + for _, item := range metaDataList { + if cfgValue, err := c.store.Get(item.Name); err == nil { + results = append(results, *cfgValue) + } + } + return results +} + +// Load - Load configuration from storage, like database or redis +func (c *CfgManager) Load() error { + return c.store.Load() +} + +// Save - Save all current configuration to storage +func (c *CfgManager) Save() error { + return c.store.Save() +} + +// Get ... +func (c *CfgManager) Get(key string) *metadata.ConfigureValue { + configValue, err := c.store.Get(key) + if err != nil { + log.Errorf("failed to get key %v, error: %v", key, err) + configValue = &metadata.ConfigureValue{} + } + return configValue +} + +// Set ... +func (c *CfgManager) Set(key string, value interface{}) { + configValue, err := metadata.NewCfgValue(key, fmt.Sprintf("%v", value)) + if err != nil { + log.Errorf("error when setting key: %v, error %v", key, err) + return + } + c.store.Set(key, *configValue) +} + +// GetDatabaseCfg - Get database configurations +/* + In database related testing, call it in the TestMain to initialize database schema and set testing configures + + cfgMgr := config.NewDBCfgManager() + dao.InitDatabase(cfgMgr.GetDatabaseCfg()) + cfgMgr.Load() + cfgMrg.UpdateConfig(testingConfigs) +*/ +func (c *CfgManager) GetDatabaseCfg() *models.Database { + return &models.Database{ + Type: c.Get(common.DatabaseType).GetString(), + PostGreSQL: &models.PostGreSQL{ + Host: c.Get(common.PostGreSQLHOST).GetString(), + Port: c.Get(common.PostGreSQLPort).GetInt(), + Username: c.Get(common.PostGreSQLUsername).GetString(), + Password: c.Get(common.PostGreSQLPassword).GetString(), + Database: c.Get(common.PostGreSQLDatabase).GetString(), + SSLMode: c.Get(common.PostGreSQLSSLMode).GetString(), + }, + } +} + +// UpdateConfig - Update config store with a specified configuration and also save updated configure +func (c *CfgManager) UpdateConfig(cfgs map[string]interface{}) error { + return c.store.Update(cfgs) +} diff --git a/src/common/config/manager_test.go b/src/common/config/manager_test.go new file mode 100644 index 000000000..6c3c783d8 --- /dev/null +++ b/src/common/config/manager_test.go @@ -0,0 +1,113 @@ +package config + +import ( + "fmt" + "github.com/goharbor/harbor/src/common/dao" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +var TestDBConfig = map[string]interface{}{ + "postgresql_host": "localhost", + "postgresql_database": "registry", + "postgresql_password": "root123", + "postgresql_username": "postgres", + "postgresql_sslmode": "disable", + "email_host": "127.0.0.1", + "clair_url": "http://clair:6060", +} + +var configManager *CfgManager + +func TestMain(m *testing.M) { + configManager = NewDBCfgManager() + dao.InitDatabase(configManager.GetDatabaseCfg()) + configManager.UpdateConfig(TestDBConfig) + os.Exit(m.Run()) +} + +func TestLoadFromDatabase(t *testing.T) { + + dao.InitDatabase(configManager.GetDatabaseCfg()) + configManager.Load() + configManager.UpdateConfig(TestDBConfig) + assert.Equal(t, "127.0.0.1", configManager.Get("email_host").GetString()) + assert.Equal(t, "http://clair:6060", configManager.Get("clair_url").GetString()) +} + +func TestSaveToDatabase(t *testing.T) { + dao.InitDatabase(configManager.GetDatabaseCfg()) + fmt.Printf("database config %#v\n", configManager.GetDatabaseCfg()) + configManager.Load() + configManager.Set("read_only", "true") + configManager.UpdateConfig(TestDBConfig) + configManager.Save() + configManager.Load() + assert.Equal(t, true, configManager.Get("read_only").GetBool()) +} + +func TestUpdateCfg(t *testing.T) { + testConfig := map[string]interface{}{ + "ldap_url": "ldaps://ldap.vmware.com", + "ldap_search_dn": "cn=admin,dc=example,dc=com", + "ldap_timeout": 10, + "ldap_search_password": "admin", + "ldap_base_dn": "dc=example,dc=com", + } + dao.InitDatabase(configManager.GetDatabaseCfg()) + configManager.Load() + configManager.UpdateConfig(testConfig) + + assert.Equal(t, "ldaps://ldap.vmware.com", configManager.Get("ldap_url").GetString()) + assert.Equal(t, 10, configManager.Get("ldap_timeout").GetInt()) + assert.Equal(t, "admin", configManager.Get("ldap_search_password").GetPassword()) + assert.Equal(t, "cn=admin,dc=example,dc=com", configManager.Get("ldap_search_dn").GetString()) + assert.Equal(t, "dc=example,dc=com", configManager.Get("ldap_base_dn").GetString()) +} + +func TestCfgManager_loadDefaultValues(t *testing.T) { + configManager.loadDefault() + if configManager.Get("ldap_timeout").GetInt() != 5 { + t.Errorf("Failed to load ldap_timeout") + } +} + +func TestCfgManger_loadSystemValues(t *testing.T) { + // os.Setenv("CLAIR_DB", "mysql") + configManager.loadDefault() + configManager.loadSystemConfigFromEnv() + configManager.UpdateConfig(map[string]interface{}{ + "clair_db": "mysql", + }) + if configManager.Get("clair_db").GetString() != "mysql" { + t.Errorf("Failed to set system value clair_db, expected %v, actual %v", "mysql", configManager.Get("clair_db").GetString()) + } +} +func TestCfgManager_GetDatabaseCfg(t *testing.T) { + configManager.UpdateConfig(map[string]interface{}{ + "postgresql_host": "localhost", + "postgresql_database": "registry", + "postgresql_password": "root123", + "postgresql_username": "postgres", + "postgresql_sslmode": "disable", + }) + dbCfg := configManager.GetDatabaseCfg() + assert.Equal(t, "localhost", dbCfg.PostGreSQL.Host) + assert.Equal(t, "registry", dbCfg.PostGreSQL.Database) + assert.Equal(t, "root123", dbCfg.PostGreSQL.Password) + assert.Equal(t, "postgres", dbCfg.PostGreSQL.Username) + assert.Equal(t, "disable", dbCfg.PostGreSQL.SSLMode) +} + +func TestNewInMemoryManager(t *testing.T) { + inMemoryManager := NewInMemoryManager() + inMemoryManager.UpdateConfig(map[string]interface{}{ + "ldap_url": "ldaps://ldap.vmware.com", + "ldap_timeout": 5, + "ldap_verify_cert": true, + }) + assert.Equal(t, "ldaps://ldap.vmware.com", inMemoryManager.Get("ldap_url").GetString()) + assert.Equal(t, 5, inMemoryManager.Get("ldap_timeout").GetInt()) + assert.Equal(t, true, inMemoryManager.Get("ldap_verify_cert").GetBool()) +} diff --git a/src/common/config/metadata/value.go b/src/common/config/metadata/value.go index 44e7ee79b..ccec3cd96 100644 --- a/src/common/config/metadata/value.go +++ b/src/common/config/metadata/value.go @@ -37,15 +37,14 @@ type ConfigureValue struct { Value string `json:"value,omitempty"` } -// NewConfigureValue ... -func NewConfigureValue(name, value string) *ConfigureValue { +// NewCfgValue ... Create checked config value +func NewCfgValue(name, value string) (*ConfigureValue, error) { result := &ConfigureValue{} err := result.Set(name, value) if err != nil { - log.Errorf("Failed to set name:%v, value:%v, error %v", name, value, err) result.Name = name // Keep name to trace error } - return result + return result, err } // GetString - Get the string value of current configure @@ -74,7 +73,7 @@ func (c *ConfigureValue) GetInt() int { return intValue } } - log.Errorf("The current value's metadata is not defined, %+v", c) + log.Errorf("GetInt failed, the current value's metadata is not defined, %+v", c) return 0 } @@ -90,7 +89,7 @@ func (c *ConfigureValue) GetInt64() int64 { return int64Value } } - log.Errorf("The current value's metadata is not defined, %+v", c) + log.Errorf("GetInt64 failed, the current value's metadata is not defined, %+v", c) return 0 } @@ -106,7 +105,7 @@ func (c *ConfigureValue) GetBool() bool { return boolValue } } - log.Errorf("The current value's metadata is not defined, %+v", c) + log.Errorf("GetBool failed, the current value's metadata is not defined, %+v", c) return false } @@ -116,7 +115,7 @@ func (c *ConfigureValue) GetStringToStringMap() map[string]string { if item, ok := Instance().GetByName(c.Name); ok { val, err := item.ItemType.get(c.Value) if err != nil { - log.Errorf("The GetBool failed, error: %+v", err) + log.Errorf("The GetStringToStringMap failed, error: %+v", err) return result } if mapValue, suc := val.(map[string]string); suc { diff --git a/src/common/config/metadata/value_test.go b/src/common/config/metadata/value_test.go index a201a36ad..f12398ecc 100644 --- a/src/common/config/metadata/value_test.go +++ b/src/common/config/metadata/value_test.go @@ -15,6 +15,7 @@ package metadata import ( + "fmt" "github.com/stretchr/testify/assert" "testing" ) @@ -27,25 +28,36 @@ var testingMetaDataArray = []Item{ {Name: "sample_map_setting", ItemType: &MapType{}, Scope: "user", Group: "ldapbasic"}, } +// createCfgValue ... Create a ConfigureValue object, only used in test +func createCfgValue(name, value string) *ConfigureValue { + result := &ConfigureValue{} + err := result.Set(name, value) + if err != nil { + fmt.Printf("failed to create ConfigureValue name:%v, value:%v, error %v\n", name, value, err) + result.Name = name // Keep name to trace error + } + return result +} + func TestConfigureValue_GetBool(t *testing.T) { - assert.Equal(t, NewConfigureValue("ldap_verify_cert", "true").GetBool(), true) - assert.Equal(t, NewConfigureValue("unknown", "false").GetBool(), false) + assert.Equal(t, createCfgValue("ldap_verify_cert", "true").GetBool(), true) + assert.Equal(t, createCfgValue("unknown", "false").GetBool(), false) } func TestConfigureValue_GetString(t *testing.T) { - assert.Equal(t, NewConfigureValue("ldap_url", "ldaps://ldap.vmware.com").GetString(), "ldaps://ldap.vmware.com") + assert.Equal(t, createCfgValue("ldap_url", "ldaps://ldap.vmware.com").GetString(), "ldaps://ldap.vmware.com") } func TestConfigureValue_GetStringToStringMap(t *testing.T) { Instance().initFromArray(testingMetaDataArray) - assert.Equal(t, NewConfigureValue("sample_map_setting", `{"sample":"abc"}`).GetStringToStringMap(), map[string]string{"sample": "abc"}) + assert.Equal(t, createCfgValue("sample_map_setting", `{"sample":"abc"}`).GetStringToStringMap(), map[string]string{"sample": "abc"}) Instance().init() } func TestConfigureValue_GetInt(t *testing.T) { - assert.Equal(t, NewConfigureValue("ldap_timeout", "5").GetInt(), 5) + assert.Equal(t, createCfgValue("ldap_timeout", "5").GetInt(), 5) } func TestConfigureValue_GetInt64(t *testing.T) { Instance().initFromArray(testingMetaDataArray) - assert.Equal(t, NewConfigureValue("ulimit", "99999").GetInt64(), int64(99999)) + assert.Equal(t, createCfgValue("ulimit", "99999").GetInt64(), int64(99999)) } diff --git a/src/common/config/store/driver/db.go b/src/common/config/store/driver/db.go new file mode 100644 index 000000000..ada4ac4a8 --- /dev/null +++ b/src/common/config/store/driver/db.go @@ -0,0 +1,81 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package driver + +import ( + "fmt" + "github.com/goharbor/harbor/src/common/config/encrypt" + "github.com/goharbor/harbor/src/common/config/metadata" + "github.com/goharbor/harbor/src/common/dao" + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/utils/log" +) + +// Database - Used to load/save configuration in database +type Database struct { +} + +// Load - load config from database, only user setting will be load from database. +func (d *Database) Load() (map[string]interface{}, error) { + resultMap := map[string]interface{}{} + configEntries, err := dao.GetConfigEntries() + if err != nil { + return resultMap, err + } + for _, item := range configEntries { + + itemMetadata, ok := metadata.Instance().GetByName(item.Key) + if !ok { + log.Warningf("failed to get metadata, key:%v, error:%v, skip to load item", item.Key, err) + continue + } + if itemMetadata.Scope == metadata.SystemScope { + continue + } + if _, ok := itemMetadata.ItemType.(*metadata.PasswordType); ok { + if decryptPassword, err := encrypt.Instance().Decrypt(item.Value); err == nil { + item.Value = decryptPassword + } else { + log.Errorf("decrypt password failed, error %v", err) + } + } + resultMap[itemMetadata.Name] = item.Value + } + return resultMap, nil +} + +// Save - Only save user config items in the cfgs map +func (d *Database) Save(cfgs map[string]interface{}) error { + var configEntries []models.ConfigEntry + for key, value := range cfgs { + if item, ok := metadata.Instance().GetByName(key); ok { + if item.Scope == metadata.SystemScope { + log.Errorf("system setting can not updated, key %v", key) + continue + } + strValue := fmt.Sprintf("%v", value) + entry := &models.ConfigEntry{Key: key, Value: strValue} + if _, ok := item.ItemType.(*metadata.PasswordType); ok { + if encryptPassword, err := encrypt.Instance().Encrypt(strValue); err == nil { + entry.Value = encryptPassword + } + } + configEntries = append(configEntries, *entry) + } else { + log.Errorf("failed to get metadata, skip to save key:%v", key) + } + } + return dao.SaveConfigEntries(configEntries) +} diff --git a/src/common/config/store/driver/db_test.go b/src/common/config/store/driver/db_test.go new file mode 100644 index 000000000..018edd225 --- /dev/null +++ b/src/common/config/store/driver/db_test.go @@ -0,0 +1,59 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package driver + +import ( + "github.com/goharbor/harbor/src/common/dao" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestMain(m *testing.M) { + dao.PrepareTestForPostgresSQL() + os.Exit(m.Run()) +} + +func TestDatabase_Load(t *testing.T) { + driver := Database{} + cfgMap, err := driver.Load() + if err != nil { + t.Errorf("failed to load, error %v", err) + } + + assert.True(t, len(cfgMap) > 10) + + if _, ok := cfgMap["ldap_url"]; !ok { + t.Error("Can not find ldap_url") + } + +} + +func TestDatabase_Save(t *testing.T) { + ldapURL := "ldap://ldap.vmware.com" + driver := Database{} + prevCfg, err := driver.Load() + if err != nil { + t.Errorf("failed to load config %v", err) + } + cfgMap := map[string]interface{}{"ldap_url": ldapURL} + driver.Save(cfgMap) + updatedMap, err := driver.Load() + if err != nil { + t.Errorf("failed to load config %v", err) + } + assert.Equal(t, updatedMap["ldap_url"], ldapURL) + driver.Save(prevCfg) + +} diff --git a/src/common/config/store/driver/driver.go b/src/common/config/store/driver/driver.go new file mode 100644 index 000000000..505c77c6e --- /dev/null +++ b/src/common/config/store/driver/driver.go @@ -0,0 +1,10 @@ +// Package driver provide the implementation of config driver used in CfgManager +package driver + +// Driver the interface to save/load config +type Driver interface { + // Load - load config item from config driver + Load() (map[string]interface{}, error) + // Save - save config item into config driver + Save(cfg map[string]interface{}) error +} diff --git a/src/common/config/store/driver/rest.go b/src/common/config/store/driver/rest.go new file mode 100644 index 000000000..f687bc247 --- /dev/null +++ b/src/common/config/store/driver/rest.go @@ -0,0 +1,29 @@ +package driver + +import ( + "github.com/goharbor/harbor/src/common/http" + "github.com/goharbor/harbor/src/common/http/modifier" +) + +// RESTDriver - config store driver based on REST API +type RESTDriver struct { + coreURL string + client *http.Client +} + +// NewRESTDriver - Create RESTDriver +func NewRESTDriver(coreURL string, modifiers ...modifier.Modifier) *RESTDriver { + return &RESTDriver{coreURL: coreURL, client: http.NewClient(nil, modifiers...)} +} + +// Load - load config data from REST server +func (h *RESTDriver) Load() (map[string]interface{}, error) { + cfgMap := map[string]interface{}{} + err := h.client.Get(h.coreURL, &cfgMap) + return cfgMap, err +} + +// Save - Save config data to REST server by PUT method +func (h *RESTDriver) Save(cfgMap map[string]interface{}) error { + return h.client.Put(h.coreURL, cfgMap) +} diff --git a/src/common/config/store/driver/rest_test.go b/src/common/config/store/driver/rest_test.go new file mode 100644 index 000000000..445fd21f0 --- /dev/null +++ b/src/common/config/store/driver/rest_test.go @@ -0,0 +1,75 @@ +package driver + +import ( + "encoding/json" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/stretchr/testify/assert" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +func ConfigGetHandler(w http.ResponseWriter, r *http.Request) { + cfgs := map[string]interface{}{ + "ldap_url": "ldaps://ldap.vmware.com", + "ldap_scope": 5, + "ldap_verify_cert": true, + } + b, err := json.Marshal(cfgs) + if err != nil { + return + } + w.Write(b) +} + +func TestHTTPDriver_Load(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(ConfigGetHandler)) + defer server.Close() + httpDriver := NewRESTDriver(server.URL) + configMap, err := httpDriver.Load() + if err != nil { + t.Errorf("Error when testing http driver %v", err) + } + assert.Equal(t, "ldaps://ldap.vmware.com", configMap["ldap_url"]) + // json.Marshal() always convert number to float64, configvalue can handle it by convert it to string + assert.Equal(t, float64(5), configMap["ldap_scope"]) + assert.Equal(t, true, configMap["ldap_verify_cert"]) +} + +var configMapForTest = map[string]interface{}{} + +func ConfigPutHandler(w http.ResponseWriter, r *http.Request) { + cfgs := map[string]interface{}{} + content, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Fatal(err) + } + err = json.Unmarshal(content, &cfgs) + if err != nil { + log.Fatal(err) + } + for k, v := range cfgs { + configMapForTest[k] = v + } +} + +func TestHTTPDriver_Save(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(ConfigPutHandler)) + defer server.Close() + httpDriver := NewRESTDriver(server.URL) + configMap := map[string]interface{}{ + "ldap_url": "ldap://www.example.com", + "ldap_timeout": 10, + "ldap_verify_cert": false, + } + err := httpDriver.Save(configMap) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, "ldap://www.example.com", configMapForTest["ldap_url"]) + assert.Equal(t, float64(10), configMapForTest["ldap_timeout"]) + assert.Equal(t, false, configMapForTest["ldap_verify_cert"]) + +} diff --git a/src/common/config/store/store.go b/src/common/config/store/store.go new file mode 100644 index 000000000..9f55efece --- /dev/null +++ b/src/common/config/store/store.go @@ -0,0 +1,102 @@ +// Package store is only used in the internal implement of manager, not a public api. +package store + +import ( + "errors" + "fmt" + "github.com/goharbor/harbor/src/common/config/metadata" + "github.com/goharbor/harbor/src/common/config/store/driver" + "github.com/goharbor/harbor/src/common/utils/log" + "sync" +) + +// ConfigStore - the config data store +type ConfigStore struct { + cfgDriver driver.Driver + cfgValues sync.Map +} + +// NewConfigStore create config store +func NewConfigStore(cfgDriver driver.Driver) *ConfigStore { + return &ConfigStore{cfgDriver: cfgDriver} +} + +// Get - Get config data from current store +func (c *ConfigStore) Get(key string) (*metadata.ConfigureValue, error) { + if value, ok := c.cfgValues.Load(key); ok { + if result, ok := value.(metadata.ConfigureValue); ok { + return &result, nil + } + return nil, errors.New("data in config store is not a ConfigureValue type") + } + return nil, metadata.ErrValueNotSet + +} + +// Set - Set configure value in store, not saved to config driver +func (c *ConfigStore) Set(key string, value metadata.ConfigureValue) error { + c.cfgValues.Store(key, value) + return nil +} + +// Load - Load data from driver, all user config in the store will be refreshed +func (c *ConfigStore) Load() error { + if c.cfgDriver == nil { + return errors.New("failed to load store, cfgDriver is nil") + } + cfgs, err := c.cfgDriver.Load() + if err != nil { + return err + } + for key, value := range cfgs { + cfgValue := metadata.ConfigureValue{} + strValue := fmt.Sprintf("%v", value) + err = cfgValue.Set(key, strValue) + if err != nil { + log.Errorf("error when loading data item, key %v, value %v, error %v", key, value, err) + continue + } + c.cfgValues.Store(key, cfgValue) + } + return nil +} + +// Save - Save all data in current store +func (c *ConfigStore) Save() error { + cfgMap := map[string]interface{}{} + c.cfgValues.Range(func(key, value interface{}) bool { + keyStr := fmt.Sprintf("%v", key) + if configValue, ok := value.(metadata.ConfigureValue); ok { + valueStr := configValue.Value + if _, ok := metadata.Instance().GetByName(keyStr); ok { + cfgMap[keyStr] = valueStr + } else { + + log.Errorf("failed to get metadata for key %v", keyStr) + } + } + return true + }) + + if c.cfgDriver == nil { + return errors.New("failed to save store, cfgDriver is nil") + } + + return c.cfgDriver.Save(cfgMap) +} + +// Update - Only update specified settings in cfgMap in store and driver +func (c *ConfigStore) Update(cfgMap map[string]interface{}) error { + // Update to store + for key, value := range cfgMap { + configValue, err := metadata.NewCfgValue(key, fmt.Sprintf("%v", value)) + if err != nil { + log.Warningf("error %v, skip to update configure item, key:%v ", err, key) + delete(cfgMap, key) + continue + } + c.Set(key, *configValue) + } + // Update to driver + return c.cfgDriver.Save(cfgMap) +} diff --git a/src/common/config/store/store_test.go b/src/common/config/store/store_test.go new file mode 100644 index 000000000..efc3c772c --- /dev/null +++ b/src/common/config/store/store_test.go @@ -0,0 +1,52 @@ +package store + +import ( + "github.com/goharbor/harbor/src/common/config/metadata" + "github.com/goharbor/harbor/src/common/config/store/driver" + "github.com/goharbor/harbor/src/common/dao" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestMain(m *testing.M) { + dao.PrepareTestForPostgresSQL() + cfgStore := NewConfigStore(&driver.Database{}) + cfgStore.Set("ldap_url", metadata.ConfigureValue{Name: "ldap_url", Value: "ldap://ldap.vmware.com"}) + err := cfgStore.Save() + if err != nil { + log.Fatal(err) + } + + os.Exit(m.Run()) +} +func TestConfigStore_Save(t *testing.T) { + cfgStore := NewConfigStore(&driver.Database{}) + err := cfgStore.Save() + cfgStore.Set("ldap_verify_cert", metadata.ConfigureValue{Name: "ldap_verify_cert", Value: "true"}) + if err != nil { + t.Fatal(err) + } + cfgValue, err := cfgStore.Get("ldap_verify_cert") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, true, cfgValue.GetBool()) + +} + +func TestConfigStore_Load(t *testing.T) { + cfgStore := NewConfigStore(&driver.Database{}) + err := cfgStore.Load() + if err != nil { + t.Fatal(err) + } + cfgValue, err := cfgStore.Get("ldap_url") + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, "ldap://ldap.vmware.com", cfgValue.GetString()) + +}