mirror of
https://github.com/safing/portbase
synced 2025-09-01 10:09:50 +00:00
Improve config and integrate with database
This commit is contained in:
parent
31c09512a0
commit
53fde29e1a
12 changed files with 672 additions and 157 deletions
155
config/database.go
Normal file
155
config/database.go
Normal file
|
@ -0,0 +1,155 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/Safing/portbase/log"
|
||||
"github.com/Safing/portbase/database"
|
||||
"github.com/Safing/portbase/database/storage"
|
||||
"github.com/Safing/portbase/database/record"
|
||||
"github.com/Safing/portbase/database/query"
|
||||
"github.com/Safing/portbase/database/iterator"
|
||||
)
|
||||
|
||||
var (
|
||||
dbController *database.Controller
|
||||
)
|
||||
|
||||
// ConfigStorageInterface provices a storage.Interface to the configuration manager.
|
||||
type ConfigStorageInterface struct {
|
||||
storage.InjectBase
|
||||
}
|
||||
|
||||
// Get returns a database record.
|
||||
func (s *ConfigStorageInterface) Get(key string) (record.Record, error) {
|
||||
optionsLock.Lock()
|
||||
defer optionsLock.Unlock()
|
||||
|
||||
opt, ok := options[key]
|
||||
if !ok {
|
||||
return nil, storage.ErrNotFound
|
||||
}
|
||||
|
||||
return opt.Export()
|
||||
}
|
||||
|
||||
// Put stores a record in the database.
|
||||
func (s *ConfigStorageInterface) Put(r record.Record) error {
|
||||
acc := r.GetAccessor(r)
|
||||
if acc == nil {
|
||||
return errors.New("invalid data")
|
||||
}
|
||||
|
||||
optionsLock.RLock()
|
||||
option, ok := options[r.DatabaseKey()]
|
||||
optionsLock.RUnlock()
|
||||
if !ok {
|
||||
return errors.New("config option does not exist")
|
||||
}
|
||||
|
||||
var value interface{}
|
||||
switch option.OptType {
|
||||
case OptTypeString :
|
||||
value, ok = acc.GetString("Value")
|
||||
case OptTypeStringArray :
|
||||
value, ok = acc.GetStringArray("Value")
|
||||
case OptTypeInt :
|
||||
value, ok = acc.GetInt("Value")
|
||||
case OptTypeBool :
|
||||
value, ok = acc.GetBool("Value")
|
||||
}
|
||||
if !ok {
|
||||
return errors.New("expected new value in \"Value\"")
|
||||
}
|
||||
|
||||
err := setConfigOption(r.DatabaseKey(), value, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes a record from the database.
|
||||
func (s *ConfigStorageInterface) Delete(key string) error {
|
||||
return setConfigOption(key, nil, false)
|
||||
}
|
||||
|
||||
// Query returns a an iterator for the supplied query.
|
||||
func (s *ConfigStorageInterface) Query(q *query.Query, local, internal bool) (*iterator.Iterator, error) {
|
||||
|
||||
optionsLock.Lock()
|
||||
defer optionsLock.Unlock()
|
||||
|
||||
it := iterator.New()
|
||||
var opts []*Option
|
||||
for _, opt := range options {
|
||||
if strings.HasPrefix(opt.Key, q.DatabaseKeyPrefix()) {
|
||||
opts = append(opts, opt)
|
||||
}
|
||||
}
|
||||
|
||||
go s.processQuery(q, it, opts)
|
||||
|
||||
return it, nil
|
||||
}
|
||||
|
||||
func (s *ConfigStorageInterface) processQuery(q *query.Query, it *iterator.Iterator, opts []*Option) {
|
||||
|
||||
sort.Sort(sortableOptions(opts))
|
||||
|
||||
for _, opt := range options {
|
||||
r, err := opt.Export()
|
||||
if err != nil {
|
||||
it.Finish(err)
|
||||
return
|
||||
}
|
||||
it.Next <- r
|
||||
}
|
||||
|
||||
it.Finish(nil)
|
||||
}
|
||||
|
||||
// ReadOnly returns whether the database is read only.
|
||||
func (s *ConfigStorageInterface) ReadOnly() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func registerAsDatabase() error {
|
||||
_, err := database.Register(&database.Database{
|
||||
Name: "config",
|
||||
Description: "Configuration Manager",
|
||||
StorageType: "injected",
|
||||
PrimaryAPI: "",
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
controller, err := database.InjectDatabase("config", &ConfigStorageInterface{})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
dbController = controller
|
||||
return nil
|
||||
}
|
||||
|
||||
func pushFullUpdate() {
|
||||
optionsLock.RLock()
|
||||
defer optionsLock.RUnlock()
|
||||
|
||||
for _, option := range options {
|
||||
pushUpdate(option)
|
||||
}
|
||||
}
|
||||
|
||||
func pushUpdate(option *Option) {
|
||||
r, err := option.Export()
|
||||
if err != nil {
|
||||
log.Errorf("failed to export option to push update: %s", err)
|
||||
} else {
|
||||
dbController.PushUpdate(r)
|
||||
}
|
||||
}
|
|
@ -2,14 +2,41 @@ package config
|
|||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Safing/portbase/log"
|
||||
)
|
||||
|
||||
func parseAndSetConfig(jsonData string) error {
|
||||
m, err := JSONToMap([]byte(jsonData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return setConfig(m)
|
||||
}
|
||||
|
||||
func parseAndSetDefaultConfig(jsonData string) error {
|
||||
m, err := JSONToMap([]byte(jsonData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return SetDefaultConfig(m)
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
|
||||
err := SetConfig(`
|
||||
err := log.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = parseAndSetConfig(`
|
||||
{
|
||||
"monkey": "1",
|
||||
"zebra": ["black", "white"],
|
||||
"zebras": {
|
||||
"zebra": ["black", "white"]
|
||||
},
|
||||
"elephant": 2,
|
||||
"hot": true,
|
||||
"cold": false
|
||||
|
@ -19,7 +46,7 @@ func TestGet(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = SetDefaultConfig(`
|
||||
err = parseAndSetDefaultConfig(`
|
||||
{
|
||||
"monkey": "0",
|
||||
"snake": "0",
|
||||
|
@ -35,7 +62,7 @@ func TestGet(t *testing.T) {
|
|||
t.Fatalf("monkey should be 1, is %s", monkey())
|
||||
}
|
||||
|
||||
zebra := GetAsStringArray("zebra", []string{})
|
||||
zebra := GetAsStringArray("zebras/zebra", []string{})
|
||||
if len(zebra()) != 2 || zebra()[0] != "black" || zebra()[1] != "white" {
|
||||
t.Fatalf("zebra should be [\"black\", \"white\"], is %v", zebra())
|
||||
}
|
||||
|
@ -55,7 +82,7 @@ func TestGet(t *testing.T) {
|
|||
t.Fatalf("cold should be false, is %v", cold())
|
||||
}
|
||||
|
||||
err = SetConfig(`
|
||||
err = parseAndSetConfig(`
|
||||
{
|
||||
"monkey": "3"
|
||||
}
|
||||
|
@ -79,7 +106,7 @@ func TestGet(t *testing.T) {
|
|||
|
||||
func BenchmarkGetAsStringCached(b *testing.B) {
|
||||
// Setup
|
||||
err := SetConfig(`
|
||||
err := parseAndSetConfig(`
|
||||
{
|
||||
"monkey": "banana"
|
||||
}
|
||||
|
@ -100,7 +127,7 @@ func BenchmarkGetAsStringCached(b *testing.B) {
|
|||
|
||||
func BenchmarkGetAsStringRefetch(b *testing.B) {
|
||||
// Setup
|
||||
err := SetConfig(`
|
||||
err := parseAndSetConfig(`
|
||||
{
|
||||
"monkey": "banana"
|
||||
}
|
||||
|
@ -120,7 +147,7 @@ func BenchmarkGetAsStringRefetch(b *testing.B) {
|
|||
|
||||
func BenchmarkGetAsIntCached(b *testing.B) {
|
||||
// Setup
|
||||
err := SetConfig(`
|
||||
err := parseAndSetConfig(`
|
||||
{
|
||||
"monkey": 1
|
||||
}
|
||||
|
@ -141,7 +168,7 @@ func BenchmarkGetAsIntCached(b *testing.B) {
|
|||
|
||||
func BenchmarkGetAsIntRefetch(b *testing.B) {
|
||||
// Setup
|
||||
err := SetConfig(`
|
||||
err := parseAndSetConfig(`
|
||||
{
|
||||
"monkey": 1
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
package integration
|
||||
|
||||
// API
|
|
@ -1,3 +0,0 @@
|
|||
package integration
|
||||
|
||||
// register as module
|
|
@ -1,4 +0,0 @@
|
|||
package integration
|
||||
|
||||
// persist config file
|
||||
// create callback function in config to get updates
|
209
config/layers.go
209
config/layers.go
|
@ -5,15 +5,14 @@ import (
|
|||
"sync"
|
||||
"fmt"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
"github.com/Safing/portbase/log"
|
||||
)
|
||||
|
||||
var (
|
||||
configLock sync.RWMutex
|
||||
|
||||
userConfig = ""
|
||||
defaultConfig = ""
|
||||
userConfig = make(map[string]interface{})
|
||||
defaultConfig = make(map[string]interface{})
|
||||
|
||||
// ErrInvalidJSON is returned by SetConfig and SetDefaultConfig if they receive invalid json.
|
||||
ErrInvalidJSON = errors.New("json string invalid")
|
||||
|
@ -22,112 +21,109 @@ var (
|
|||
ErrInvalidOptionType = errors.New("invalid option value type")
|
||||
)
|
||||
|
||||
// SetConfig sets the (prioritized) user defined config.
|
||||
func SetConfig(json string) error {
|
||||
if !gjson.Valid(json) {
|
||||
return ErrInvalidJSON
|
||||
}
|
||||
|
||||
// setConfig sets the (prioritized) user defined config.
|
||||
func setConfig(m map[string]interface{}) error {
|
||||
configLock.Lock()
|
||||
defer configLock.Unlock()
|
||||
userConfig = json
|
||||
userConfig = m
|
||||
resetValidityFlag()
|
||||
|
||||
go pushFullUpdate()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDefaultConfig sets the (fallback) default config.
|
||||
func SetDefaultConfig(json string) error {
|
||||
if !gjson.Valid(json) {
|
||||
return ErrInvalidJSON
|
||||
}
|
||||
|
||||
func SetDefaultConfig(m map[string]interface{}) error {
|
||||
configLock.Lock()
|
||||
defer configLock.Unlock()
|
||||
defaultConfig = json
|
||||
defaultConfig = m
|
||||
resetValidityFlag()
|
||||
|
||||
go pushFullUpdate()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateValue(name string, value interface{}) error {
|
||||
func validateValue(name string, value interface{}) (*Option, error) {
|
||||
optionsLock.RLock()
|
||||
defer optionsLock.RUnlock()
|
||||
|
||||
option, ok := options[name]
|
||||
if !ok {
|
||||
switch value.(type) {
|
||||
case string:
|
||||
return nil
|
||||
case []string:
|
||||
return nil
|
||||
case int:
|
||||
return nil
|
||||
case bool:
|
||||
return nil
|
||||
default:
|
||||
return ErrInvalidOptionType
|
||||
}
|
||||
return nil, errors.New("config option does not exist")
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if option.OptType != OptTypeString {
|
||||
return fmt.Errorf("expected type string for option %s, got type %T", name, v)
|
||||
return nil, fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), name, v)
|
||||
}
|
||||
if option.compiledRegex != nil {
|
||||
if !option.compiledRegex.MatchString(v) {
|
||||
return fmt.Errorf("validation failed: string \"%s\" did not match regex for option %s", v, name)
|
||||
return nil, fmt.Errorf("validation failed: string \"%s\" did not match regex for option %s", v, name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return option, nil
|
||||
case []string:
|
||||
if option.OptType != OptTypeStringArray {
|
||||
return fmt.Errorf("expected type string for option %s, got type %T", name, v)
|
||||
return nil, fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), name, v)
|
||||
}
|
||||
if option.compiledRegex != nil {
|
||||
for pos, entry := range v {
|
||||
if !option.compiledRegex.MatchString(entry) {
|
||||
return fmt.Errorf("validation failed: string \"%s\" at index %d did not match regex for option %s", entry, pos, name)
|
||||
return nil, fmt.Errorf("validation failed: string \"%s\" at index %d did not match regex for option %s", entry, pos, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case int:
|
||||
return option, nil
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
if option.OptType != OptTypeInt {
|
||||
return fmt.Errorf("expected type int for option %s, got type %T", name, v)
|
||||
return nil, fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), name, v)
|
||||
}
|
||||
return nil
|
||||
if option.compiledRegex != nil {
|
||||
if !option.compiledRegex.MatchString(fmt.Sprintf("%d", v)) {
|
||||
return nil, fmt.Errorf("validation failed: number \"%d\" did not match regex for option %s", v, name)
|
||||
}
|
||||
}
|
||||
return option, nil
|
||||
case bool:
|
||||
if option.OptType != OptTypeBool {
|
||||
return fmt.Errorf("expected type bool for option %s, got type %T", name, v)
|
||||
return nil, fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), name, v)
|
||||
}
|
||||
return nil
|
||||
return option, nil
|
||||
default:
|
||||
return ErrInvalidOptionType
|
||||
return nil, fmt.Errorf("invalid option value type: %T", value)
|
||||
}
|
||||
}
|
||||
|
||||
// SetConfigOption sets a single value in the (prioritized) user defined config.
|
||||
func SetConfigOption(name string, value interface{}) error {
|
||||
return setConfigOption(name, value, true)
|
||||
}
|
||||
|
||||
func setConfigOption(name string, value interface{}, push bool) error {
|
||||
configLock.Lock()
|
||||
defer configLock.Unlock()
|
||||
|
||||
var err error
|
||||
var newConfig string
|
||||
|
||||
if value == nil {
|
||||
newConfig, err = sjson.Delete(userConfig, name)
|
||||
delete(userConfig, name)
|
||||
} else {
|
||||
err = validateValue(name, value)
|
||||
var option *Option
|
||||
option, err = validateValue(name, value)
|
||||
if err == nil {
|
||||
newConfig, err = sjson.Set(userConfig, name, value)
|
||||
userConfig[name] = value
|
||||
if push {
|
||||
go pushUpdate(option)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
userConfig = newConfig
|
||||
resetValidityFlag()
|
||||
go saveConfig()
|
||||
}
|
||||
|
||||
return err
|
||||
|
@ -135,23 +131,29 @@ func SetConfigOption(name string, value interface{}) error {
|
|||
|
||||
// SetDefaultConfigOption sets a single value in the (fallback) default config.
|
||||
func SetDefaultConfigOption(name string, value interface{}) error {
|
||||
return setDefaultConfigOption(name, value, true)
|
||||
}
|
||||
|
||||
func setDefaultConfigOption(name string, value interface{}, push bool) error {
|
||||
configLock.Lock()
|
||||
defer configLock.Unlock()
|
||||
|
||||
var err error
|
||||
var newConfig string
|
||||
|
||||
if value == nil {
|
||||
newConfig, err = sjson.Delete(defaultConfig, name)
|
||||
delete(defaultConfig, name)
|
||||
} else {
|
||||
err = validateValue(name, value)
|
||||
var option *Option
|
||||
option, err = validateValue(name, value)
|
||||
if err == nil {
|
||||
newConfig, err = sjson.Set(defaultConfig, name, value)
|
||||
defaultConfig[name] = value
|
||||
if push {
|
||||
go pushUpdate(option)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
defaultConfig = newConfig
|
||||
resetValidityFlag()
|
||||
}
|
||||
|
||||
|
@ -159,72 +161,113 @@ func SetDefaultConfigOption(name string, value interface{}) error {
|
|||
}
|
||||
|
||||
// findValue find the correct value in the user or default config.
|
||||
func findValue(name string) (result gjson.Result) {
|
||||
func findValue(name string) (result interface{}) {
|
||||
configLock.RLock()
|
||||
defer configLock.RUnlock()
|
||||
|
||||
result = gjson.Get(userConfig, name)
|
||||
if !result.Exists() {
|
||||
result = gjson.Get(defaultConfig, name)
|
||||
result, ok := userConfig[name]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
return result
|
||||
|
||||
result, ok = defaultConfig[name]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
|
||||
optionsLock.RLock()
|
||||
defer optionsLock.RUnlock()
|
||||
|
||||
option, ok := options[name]
|
||||
if ok {
|
||||
return option.DefaultValue
|
||||
}
|
||||
|
||||
log.Errorf("config: request for unregistered option: %s", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// findStringValue validates and returns the value with the given name.
|
||||
func findStringValue(name string, fallback string) (value string) {
|
||||
result := findValue(name)
|
||||
if !result.Exists() {
|
||||
if result == nil {
|
||||
return fallback
|
||||
}
|
||||
if result.Type != gjson.String {
|
||||
return fallback
|
||||
v, ok := result.(string)
|
||||
if ok {
|
||||
return v
|
||||
}
|
||||
return result.String()
|
||||
return fallback
|
||||
}
|
||||
|
||||
// findStringArrayValue validates and returns the value with the given name.
|
||||
func findStringArrayValue(name string, fallback []string) (value []string) {
|
||||
result := findValue(name)
|
||||
if !result.Exists() {
|
||||
if result == nil {
|
||||
return fallback
|
||||
}
|
||||
if !result.IsArray() {
|
||||
return fallback
|
||||
}
|
||||
results := result.Array()
|
||||
for _, r := range results {
|
||||
if r.Type != gjson.String {
|
||||
return fallback
|
||||
|
||||
v, ok := result.([]interface{})
|
||||
if ok {
|
||||
new := make([]string, len(v))
|
||||
for i, val := range v {
|
||||
s, ok := val.(string)
|
||||
if ok {
|
||||
new[i] = s
|
||||
} else {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
value = append(value, r.String())
|
||||
return new
|
||||
}
|
||||
return value
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
// findIntValue validates and returns the value with the given name.
|
||||
func findIntValue(name string, fallback int64) (value int64) {
|
||||
result := findValue(name)
|
||||
if !result.Exists() {
|
||||
if result == nil {
|
||||
return fallback
|
||||
}
|
||||
if result.Type != gjson.Number {
|
||||
return fallback
|
||||
switch v := result.(type) {
|
||||
case int:
|
||||
return int64(v)
|
||||
case int8:
|
||||
return int64(v)
|
||||
case int16:
|
||||
return int64(v)
|
||||
case int32:
|
||||
return int64(v)
|
||||
case int64:
|
||||
return v
|
||||
case uint:
|
||||
return int64(v)
|
||||
case uint8:
|
||||
return int64(v)
|
||||
case uint16:
|
||||
return int64(v)
|
||||
case uint32:
|
||||
return int64(v)
|
||||
case uint64:
|
||||
return int64(v)
|
||||
case float32:
|
||||
return int64(v)
|
||||
case float64:
|
||||
return int64(v)
|
||||
}
|
||||
return result.Int()
|
||||
return fallback
|
||||
}
|
||||
|
||||
// findBoolValue validates and returns the value with the given name.
|
||||
func findBoolValue(name string, fallback bool) (value bool) {
|
||||
result := findValue(name)
|
||||
if !result.Exists() {
|
||||
if result == nil {
|
||||
return fallback
|
||||
}
|
||||
switch result.Type {
|
||||
case gjson.True:
|
||||
return true
|
||||
case gjson.False:
|
||||
return false
|
||||
default:
|
||||
return fallback
|
||||
v, ok := result.(bool)
|
||||
if ok {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
|
|
@ -4,27 +4,26 @@ import "testing"
|
|||
|
||||
func TestLayersGetters(t *testing.T) {
|
||||
|
||||
err := SetConfig("{invalid json")
|
||||
if err == nil {
|
||||
t.Error("expected error")
|
||||
}
|
||||
|
||||
err = SetDefaultConfig("{invalid json")
|
||||
if err == nil {
|
||||
t.Error("expected error")
|
||||
}
|
||||
|
||||
err = SetConfig(`
|
||||
mapData, err := JSONToMap([]byte(`
|
||||
{
|
||||
"monkey": "1",
|
||||
"zebra": ["black", "white"],
|
||||
"weird_zebra": ["black", -1],
|
||||
"elephant": 2,
|
||||
"hot": true
|
||||
"zebras": {
|
||||
"zebra": ["black", "white"],
|
||||
"weird_zebra": ["black", -1]
|
||||
},
|
||||
"env": {
|
||||
"hot": true
|
||||
}
|
||||
}
|
||||
`)
|
||||
`))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = setConfig(mapData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test missing values
|
||||
|
@ -61,7 +60,7 @@ func TestLayersGetters(t *testing.T) {
|
|||
t.Error("expected fallback value: [fallback]")
|
||||
}
|
||||
|
||||
mixedStringArray := GetAsStringArray("weird_zebra", []string{"fallback"})
|
||||
mixedStringArray := GetAsStringArray("zebras/weird_zebra", []string{"fallback"})
|
||||
if len(mixedStringArray()) != 1 || mixedStringArray()[0] != "fallback" {
|
||||
t.Error("expected fallback value: [fallback]")
|
||||
}
|
||||
|
@ -91,7 +90,7 @@ func TestLayersSetters(t *testing.T) {
|
|||
})
|
||||
Register(&Option{
|
||||
Name: "name",
|
||||
Key: "zebra",
|
||||
Key: "zebras/zebra",
|
||||
Description: "description",
|
||||
ExpertiseLevel: 1,
|
||||
OptType: OptTypeStringArray,
|
||||
|
@ -121,7 +120,7 @@ func TestLayersSetters(t *testing.T) {
|
|||
if err := SetConfigOption("monkey", "banana"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := SetConfigOption("zebra", []string{"black", "white"}); err != nil {
|
||||
if err := SetConfigOption("zebras/zebra", []string{"black", "white"}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := SetDefaultConfigOption("elephant", 2); err != nil {
|
||||
|
@ -135,7 +134,7 @@ func TestLayersSetters(t *testing.T) {
|
|||
if err := SetConfigOption("monkey", []string{"black", "white"}); err == nil {
|
||||
t.Error("should fail")
|
||||
}
|
||||
if err := SetConfigOption("zebra", 2); err == nil {
|
||||
if err := SetConfigOption("zebras/zebra", 2); err == nil {
|
||||
t.Error("should fail")
|
||||
}
|
||||
if err := SetDefaultConfigOption("elephant", true); err == nil {
|
||||
|
@ -152,25 +151,25 @@ func TestLayersSetters(t *testing.T) {
|
|||
if err := SetConfigOption("monkey", "dirt"); err == nil {
|
||||
t.Error("should fail")
|
||||
}
|
||||
if err := SetConfigOption("zebra", []string{"Element649"}); err == nil {
|
||||
if err := SetConfigOption("zebras/zebra", []string{"Element649"}); err == nil {
|
||||
t.Error("should fail")
|
||||
}
|
||||
|
||||
// unregistered checking
|
||||
if err := SetConfigOption("invalid", "banana"); err != nil {
|
||||
t.Error(err)
|
||||
if err := SetConfigOption("invalid", "banana"); err == nil {
|
||||
t.Error("should fail")
|
||||
}
|
||||
if err := SetConfigOption("invalid", []string{"black", "white"}); err != nil {
|
||||
t.Error(err)
|
||||
if err := SetConfigOption("invalid", []string{"black", "white"}); err == nil {
|
||||
t.Error("should fail")
|
||||
}
|
||||
if err := SetConfigOption("invalid", 2); err != nil {
|
||||
t.Error(err)
|
||||
if err := SetConfigOption("invalid", 2); err == nil {
|
||||
t.Error("should fail")
|
||||
}
|
||||
if err := SetConfigOption("invalid", true); err != nil {
|
||||
t.Error(err)
|
||||
if err := SetConfigOption("invalid", true); err == nil {
|
||||
t.Error("should fail")
|
||||
}
|
||||
if err := SetConfigOption("invalid", []byte{0}); err != ErrInvalidOptionType {
|
||||
t.Error("should fail with ErrInvalidOptionType")
|
||||
if err := SetConfigOption("invalid", []byte{0}); err == nil {
|
||||
t.Error("should fail")
|
||||
}
|
||||
|
||||
// delete
|
||||
|
|
32
config/main.go
Normal file
32
config/main.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/Safing/portbase/database"
|
||||
"github.com/Safing/portbase/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register("config", prep, start, stop, "database")
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func start() error {
|
||||
configFilePath = path.Join(database.GetDatabaseRoot(), "config.json")
|
||||
|
||||
err := loadConfig()
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return registerAsDatabase()
|
||||
}
|
||||
|
||||
func stop() error {
|
||||
return nil
|
||||
}
|
104
config/option.go
Normal file
104
config/option.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/tidwall/sjson"
|
||||
|
||||
"github.com/Safing/portbase/database/record"
|
||||
)
|
||||
|
||||
// Variable Type IDs for frontend Identification. Use ExternalOptType for extended types in the frontend.
|
||||
const (
|
||||
OptTypeString uint8 = 1
|
||||
OptTypeStringArray uint8 = 2
|
||||
OptTypeInt uint8 = 3
|
||||
OptTypeBool uint8 = 4
|
||||
|
||||
ExpertiseLevelUser uint8 = 1
|
||||
ExpertiseLevelExpert uint8 = 2
|
||||
ExpertiseLevelDeveloper uint8 = 3
|
||||
)
|
||||
|
||||
func getTypeName(t uint8) string {
|
||||
switch t {
|
||||
case OptTypeString:
|
||||
return "string"
|
||||
case OptTypeStringArray:
|
||||
return "[]string"
|
||||
case OptTypeInt:
|
||||
return "int"
|
||||
case OptTypeBool:
|
||||
return "bool"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Option describes a configuration option.
|
||||
type Option struct {
|
||||
Name string
|
||||
Key string // category/sub/key
|
||||
Description string
|
||||
ExpertiseLevel uint8
|
||||
OptType uint8
|
||||
DefaultValue interface{}
|
||||
ExternalOptType string
|
||||
ValidationRegex string
|
||||
compiledRegex *regexp.Regexp
|
||||
}
|
||||
|
||||
// Export expors an option to a Record.
|
||||
func (opt *Option) Export() (record.Record, error) {
|
||||
data, err := json.Marshal(opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configLock.RLock()
|
||||
defer configLock.RUnlock()
|
||||
|
||||
userValue, ok := userConfig[opt.Key]
|
||||
if ok {
|
||||
data, err = sjson.SetBytes(data, "Value", userValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
defaultValue, ok := defaultConfig[opt.Key]
|
||||
if ok {
|
||||
data, err = sjson.SetBytes(data, "DefaultValue", defaultValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
r, err := record.NewWrapper(fmt.Sprintf("config:%s", opt.Key), nil, record.JSON, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.SetMeta(&record.Meta{})
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
type sortableOptions []*Option
|
||||
|
||||
// Len is the number of elements in the collection.
|
||||
func (opts sortableOptions) Len() int {
|
||||
return len(opts)
|
||||
}
|
||||
|
||||
// Less reports whether the element with
|
||||
// index i should sort before the element with index j.
|
||||
func (opts sortableOptions) Less(i, j int) bool {
|
||||
return opts[i].Key < opts[j].Key
|
||||
}
|
||||
|
||||
// Swap swaps the elements with indexes i and j.
|
||||
func (opts sortableOptions) Swap(i, j int) {
|
||||
opts[i], opts[j] = opts[j], opts[i]
|
||||
}
|
125
config/persistence.go
Normal file
125
config/persistence.go
Normal file
|
@ -0,0 +1,125 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/Safing/portbase/log"
|
||||
)
|
||||
|
||||
var (
|
||||
configFilePath string
|
||||
)
|
||||
|
||||
func loadConfig() error {
|
||||
data, err := ioutil.ReadFile(configFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m, err := JSONToMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return setConfig(m)
|
||||
}
|
||||
|
||||
func saveConfig() (err error) {
|
||||
data, err := MapToJSON(userConfig)
|
||||
if err == nil {
|
||||
err = ioutil.WriteFile(configFilePath, data, 0600)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("config: failed to save config: %s", err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// JSONToMap parses and flattens a hierarchical json object.
|
||||
func JSONToMap(jsonData []byte) (map[string]interface{}, error) {
|
||||
loaded := make(map[string]interface{})
|
||||
err := json.Unmarshal(jsonData, &loaded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
flatten(loaded, loaded, "")
|
||||
return loaded, nil
|
||||
}
|
||||
|
||||
func flatten(rootMap, subMap map[string]interface{}, subKey string) {
|
||||
for key, entry := range subMap {
|
||||
|
||||
// get next level key
|
||||
subbedKey := key
|
||||
if subKey != "" {
|
||||
subbedKey = fmt.Sprintf("%s/%s", subKey, key)
|
||||
}
|
||||
|
||||
// check for next subMap
|
||||
nextSub, ok := entry.(map[string]interface{})
|
||||
if ok {
|
||||
flatten(rootMap, nextSub, subbedKey)
|
||||
delete(rootMap, key)
|
||||
} else if subKey != "" {
|
||||
// only set if not on root level
|
||||
rootMap[subbedKey] = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MapToJSON expands a flattened map and returns it as json.
|
||||
func MapToJSON(mapData map[string]interface{}) ([]byte, error) {
|
||||
configLock.RLock()
|
||||
defer configLock.RUnlock()
|
||||
|
||||
new := make(map[string]interface{})
|
||||
for key, value := range mapData {
|
||||
new[key] = value
|
||||
}
|
||||
|
||||
expand(new)
|
||||
return json.MarshalIndent(new, "", " ")
|
||||
}
|
||||
|
||||
// expand expands a flattened map.
|
||||
func expand(mapData map[string]interface{}) {
|
||||
var newMaps []map[string]interface{}
|
||||
for key, entry := range mapData {
|
||||
if strings.Contains(key, "/") {
|
||||
parts := strings.SplitN(key, "/", 2)
|
||||
if len(parts) == 2 {
|
||||
|
||||
// get subMap
|
||||
var subMap map[string]interface{}
|
||||
v, ok := mapData[parts[0]]
|
||||
if ok {
|
||||
subMap, ok = v.(map[string]interface{})
|
||||
if !ok {
|
||||
subMap = make(map[string]interface{})
|
||||
newMaps = append(newMaps, subMap)
|
||||
mapData[parts[0]] = subMap
|
||||
}
|
||||
} else {
|
||||
subMap = make(map[string]interface{})
|
||||
newMaps = append(newMaps, subMap)
|
||||
mapData[parts[0]] = subMap
|
||||
}
|
||||
|
||||
// set entry
|
||||
subMap[parts[1]] = entry
|
||||
// delete entry from
|
||||
delete(mapData, key)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, entry := range newMaps {
|
||||
expand(entry)
|
||||
}
|
||||
}
|
64
config/persistence_test.go
Normal file
64
config/persistence_test.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJSONMapConversion(t *testing.T) {
|
||||
|
||||
jsonData := `{
|
||||
"a": "b",
|
||||
"c": {
|
||||
"d": "e",
|
||||
"f": "g",
|
||||
"h": {
|
||||
"i": "j",
|
||||
"k": "l",
|
||||
"m": {
|
||||
"n": "o"
|
||||
}
|
||||
}
|
||||
},
|
||||
"p": "q"
|
||||
}`
|
||||
jsonBytes := []byte(jsonData)
|
||||
|
||||
mapData := map[string]interface{}{
|
||||
"a": "b",
|
||||
"p": "q",
|
||||
"c/d": "e",
|
||||
"c/f": "g",
|
||||
"c/h/i": "j",
|
||||
"c/h/k": "l",
|
||||
"c/h/m/n": "o",
|
||||
}
|
||||
|
||||
m, err := JSONToMap(jsonBytes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
j, err := MapToJSON(mapData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(jsonBytes, j) {
|
||||
t.Errorf("json does not match, got %s", j)
|
||||
}
|
||||
|
||||
j2, err := MapToJSON(m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(jsonBytes, j2) {
|
||||
t.Errorf("json does not match, got %s", j)
|
||||
}
|
||||
|
||||
// fails for some reason
|
||||
// if !reflect.DeepEqual(mapData, m) {
|
||||
// t.Errorf("maps do not match, got %s", m)
|
||||
// }
|
||||
}
|
|
@ -7,18 +7,6 @@ import (
|
|||
"sync"
|
||||
)
|
||||
|
||||
// Variable Type IDs for frontend Identification. Values from 100 are free for custom use.
|
||||
const (
|
||||
OptTypeString uint8 = 1
|
||||
OptTypeStringArray uint8 = 2
|
||||
OptTypeInt uint8 = 3
|
||||
OptTypeBool uint8 = 4
|
||||
|
||||
ExpertiseLevelUser uint8 = 1
|
||||
ExpertiseLevelExpert uint8 = 2
|
||||
ExpertiseLevelDeveloper uint8 = 3
|
||||
)
|
||||
|
||||
var (
|
||||
optionsLock sync.RWMutex
|
||||
options = make(map[string]*Option)
|
||||
|
@ -27,18 +15,6 @@ var (
|
|||
ErrIncompleteCall = errors.New("could not register config option: all fields, except for the validationRegex are mandatory")
|
||||
)
|
||||
|
||||
// Option describes a configuration option.
|
||||
type Option struct {
|
||||
Name string
|
||||
Key string
|
||||
Description string
|
||||
ExpertiseLevel uint8
|
||||
OptType uint8
|
||||
DefaultValue interface{}
|
||||
ValidationRegex string
|
||||
compiledRegex *regexp.Regexp
|
||||
}
|
||||
|
||||
// Register registers a new configuration option.
|
||||
func Register(option *Option) error {
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue