mirror of
https://github.com/safing/portbase
synced 2025-09-01 10:09:50 +00:00
Add release level attribute to config option, improve internal handling
This commit is contained in:
parent
fad816ca71
commit
2a1681792d
11 changed files with 611 additions and 366 deletions
146
config/get.go
146
config/get.go
|
@ -1,14 +1,7 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"github.com/safing/portbase/log"
|
||||||
|
|
||||||
"github.com/tevino/abool"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
validityFlag = abool.NewBool(true)
|
|
||||||
validityFlagLock sync.RWMutex
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -22,19 +15,6 @@ type (
|
||||||
BoolOption func() bool
|
BoolOption func() bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func getValidityFlag() *abool.AtomicBool {
|
|
||||||
validityFlagLock.RLock()
|
|
||||||
defer validityFlagLock.RUnlock()
|
|
||||||
return validityFlag
|
|
||||||
}
|
|
||||||
|
|
||||||
func resetValidityFlag() {
|
|
||||||
validityFlagLock.Lock()
|
|
||||||
defer validityFlagLock.Unlock()
|
|
||||||
validityFlag.SetTo(false)
|
|
||||||
validityFlag = abool.NewBool(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAsString returns a function that returns the wanted string with high performance.
|
// GetAsString returns a function that returns the wanted string with high performance.
|
||||||
func GetAsString(name string, fallback string) StringOption {
|
func GetAsString(name string, fallback string) StringOption {
|
||||||
valid := getValidityFlag()
|
valid := getValidityFlag()
|
||||||
|
@ -86,3 +66,127 @@ func GetAsBool(name string, fallback bool) BoolOption {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findValue find the correct value in the user or default config.
|
||||||
|
func findValue(key string) interface{} {
|
||||||
|
optionsLock.RLock()
|
||||||
|
option, ok := options[key]
|
||||||
|
optionsLock.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
log.Errorf("config: request for unregistered option: %s", key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// lock option
|
||||||
|
option.Lock()
|
||||||
|
defer option.Unlock()
|
||||||
|
|
||||||
|
// check if option is active
|
||||||
|
optionActive := true
|
||||||
|
switch getReleaseLevel() {
|
||||||
|
case ReleaseLevelStable:
|
||||||
|
// In stable, only stable is active
|
||||||
|
optionActive = option.ReleaseLevel == ReleaseLevelStable
|
||||||
|
case ReleaseLevelBeta:
|
||||||
|
// In beta, only stable and beta are active
|
||||||
|
optionActive = option.ReleaseLevel == ReleaseLevelStable || option.ReleaseLevel == ReleaseLevelBeta
|
||||||
|
case ReleaseLevelExperimental:
|
||||||
|
// In experimental, everything is active
|
||||||
|
optionActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if optionActive && option.activeValue != nil {
|
||||||
|
return option.activeValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if option.activeDefaultValue != nil {
|
||||||
|
return option.activeDefaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return option.DefaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// findStringValue validates and returns the value with the given key.
|
||||||
|
func findStringValue(key string, fallback string) (value string) {
|
||||||
|
result := findValue(key)
|
||||||
|
if result == nil {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
v, ok := result.(string)
|
||||||
|
if ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// findStringArrayValue validates and returns the value with the given key.
|
||||||
|
func findStringArrayValue(key string, fallback []string) (value []string) {
|
||||||
|
result := findValue(key)
|
||||||
|
if result == nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// findIntValue validates and returns the value with the given key.
|
||||||
|
func findIntValue(key string, fallback int64) (value int64) {
|
||||||
|
result := findValue(key)
|
||||||
|
if result == nil {
|
||||||
|
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 fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// findBoolValue validates and returns the value with the given key.
|
||||||
|
func findBoolValue(key string, fallback bool) (value bool) {
|
||||||
|
result := findValue(key)
|
||||||
|
if result == nil {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
v, ok := result.(bool)
|
||||||
|
if ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
|
@ -24,13 +24,36 @@ func parseAndSetDefaultConfig(jsonData string) error {
|
||||||
return SetDefaultConfig(m)
|
return SetDefaultConfig(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func quickRegister(t *testing.T, key string, optType uint8, defaultValue interface{}) {
|
||||||
|
err := Register(&Option{
|
||||||
|
Name: key,
|
||||||
|
Key: key,
|
||||||
|
Description: "test config",
|
||||||
|
ReleaseLevel: ReleaseLevelStable,
|
||||||
|
ExpertiseLevel: ExpertiseLevelUser,
|
||||||
|
OptType: optType,
|
||||||
|
DefaultValue: defaultValue,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGet(t *testing.T) {
|
func TestGet(t *testing.T) {
|
||||||
|
// reset
|
||||||
|
options = make(map[string]*Option)
|
||||||
|
|
||||||
err := log.Start()
|
err := log.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
quickRegister(t, "monkey", OptTypeInt, -1)
|
||||||
|
quickRegister(t, "zebras/zebra", OptTypeStringArray, []string{"a", "b"})
|
||||||
|
quickRegister(t, "elephant", OptTypeInt, -1)
|
||||||
|
quickRegister(t, "hot", OptTypeBool, false)
|
||||||
|
quickRegister(t, "cold", OptTypeBool, true)
|
||||||
|
|
||||||
err = parseAndSetConfig(`
|
err = parseAndSetConfig(`
|
||||||
{
|
{
|
||||||
"monkey": "1",
|
"monkey": "1",
|
||||||
|
@ -59,27 +82,27 @@ func TestGet(t *testing.T) {
|
||||||
|
|
||||||
monkey := GetAsString("monkey", "none")
|
monkey := GetAsString("monkey", "none")
|
||||||
if monkey() != "1" {
|
if monkey() != "1" {
|
||||||
t.Fatalf("monkey should be 1, is %s", monkey())
|
t.Errorf("monkey should be 1, is %s", monkey())
|
||||||
}
|
}
|
||||||
|
|
||||||
zebra := GetAsStringArray("zebras/zebra", []string{})
|
zebra := GetAsStringArray("zebras/zebra", []string{})
|
||||||
if len(zebra()) != 2 || zebra()[0] != "black" || zebra()[1] != "white" {
|
if len(zebra()) != 2 || zebra()[0] != "black" || zebra()[1] != "white" {
|
||||||
t.Fatalf("zebra should be [\"black\", \"white\"], is %v", zebra())
|
t.Errorf("zebra should be [\"black\", \"white\"], is %v", zebra())
|
||||||
}
|
}
|
||||||
|
|
||||||
elephant := GetAsInt("elephant", -1)
|
elephant := GetAsInt("elephant", -1)
|
||||||
if elephant() != 2 {
|
if elephant() != 2 {
|
||||||
t.Fatalf("elephant should be 2, is %d", elephant())
|
t.Errorf("elephant should be 2, is %d", elephant())
|
||||||
}
|
}
|
||||||
|
|
||||||
hot := GetAsBool("hot", false)
|
hot := GetAsBool("hot", false)
|
||||||
if !hot() {
|
if !hot() {
|
||||||
t.Fatalf("hot should be true, is %v", hot())
|
t.Errorf("hot should be true, is %v", hot())
|
||||||
}
|
}
|
||||||
|
|
||||||
cold := GetAsBool("cold", true)
|
cold := GetAsBool("cold", true)
|
||||||
if cold() {
|
if cold() {
|
||||||
t.Fatalf("cold should be false, is %v", cold())
|
t.Errorf("cold should be false, is %v", cold())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = parseAndSetConfig(`
|
err = parseAndSetConfig(`
|
||||||
|
@ -92,19 +115,126 @@ func TestGet(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if monkey() != "3" {
|
if monkey() != "3" {
|
||||||
t.Fatalf("monkey should be 0, is %s", monkey())
|
t.Errorf("monkey should be 0, is %s", monkey())
|
||||||
}
|
}
|
||||||
|
|
||||||
if elephant() != 0 {
|
if elephant() != 0 {
|
||||||
t.Fatalf("elephant should be 0, is %d", elephant())
|
t.Errorf("elephant should be 0, is %d", elephant())
|
||||||
}
|
}
|
||||||
|
|
||||||
zebra()
|
zebra()
|
||||||
hot()
|
hot()
|
||||||
|
|
||||||
|
// concurrent
|
||||||
|
GetAsString("monkey", "none")()
|
||||||
|
GetAsStringArray("zebras/zebra", []string{})()
|
||||||
|
GetAsInt("elephant", -1)()
|
||||||
|
GetAsBool("hot", false)()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReleaseLevel(t *testing.T) {
|
||||||
|
// reset
|
||||||
|
options = make(map[string]*Option)
|
||||||
|
registerReleaseLevelOption()
|
||||||
|
|
||||||
|
// setup
|
||||||
|
subsystemOption := &Option{
|
||||||
|
Name: "test subsystem",
|
||||||
|
Key: "subsystem/test",
|
||||||
|
Description: "test config",
|
||||||
|
ReleaseLevel: ReleaseLevelStable,
|
||||||
|
ExpertiseLevel: ExpertiseLevelUser,
|
||||||
|
OptType: OptTypeBool,
|
||||||
|
DefaultValue: false,
|
||||||
|
}
|
||||||
|
err := Register(subsystemOption)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = SetConfigOption("subsystem/test", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
testSubsystem := GetAsBool("subsystem/test", false)
|
||||||
|
|
||||||
|
// test option level stable
|
||||||
|
subsystemOption.ReleaseLevel = ReleaseLevelStable
|
||||||
|
err = SetConfigOption(releaseLevelKey, ReleaseLevelStable)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !testSubsystem() {
|
||||||
|
t.Error("should be active")
|
||||||
|
}
|
||||||
|
err = SetConfigOption(releaseLevelKey, ReleaseLevelBeta)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !testSubsystem() {
|
||||||
|
t.Error("should be active")
|
||||||
|
}
|
||||||
|
err = SetConfigOption(releaseLevelKey, ReleaseLevelExperimental)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !testSubsystem() {
|
||||||
|
t.Error("should be active")
|
||||||
|
}
|
||||||
|
|
||||||
|
// test option level beta
|
||||||
|
subsystemOption.ReleaseLevel = ReleaseLevelBeta
|
||||||
|
err = SetConfigOption(releaseLevelKey, ReleaseLevelStable)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if testSubsystem() {
|
||||||
|
t.Errorf("should be inactive: opt=%s system=%s", subsystemOption.ReleaseLevel, releaseLevel)
|
||||||
|
}
|
||||||
|
err = SetConfigOption(releaseLevelKey, ReleaseLevelBeta)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !testSubsystem() {
|
||||||
|
t.Error("should be active")
|
||||||
|
}
|
||||||
|
err = SetConfigOption(releaseLevelKey, ReleaseLevelExperimental)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !testSubsystem() {
|
||||||
|
t.Error("should be active")
|
||||||
|
}
|
||||||
|
|
||||||
|
// test option level experimental
|
||||||
|
subsystemOption.ReleaseLevel = ReleaseLevelExperimental
|
||||||
|
err = SetConfigOption(releaseLevelKey, ReleaseLevelStable)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if testSubsystem() {
|
||||||
|
t.Error("should be inactive")
|
||||||
|
}
|
||||||
|
err = SetConfigOption(releaseLevelKey, ReleaseLevelBeta)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if testSubsystem() {
|
||||||
|
t.Error("should be inactive")
|
||||||
|
}
|
||||||
|
err = SetConfigOption(releaseLevelKey, ReleaseLevelExperimental)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !testSubsystem() {
|
||||||
|
t.Error("should be active")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkGetAsStringCached(b *testing.B) {
|
func BenchmarkGetAsStringCached(b *testing.B) {
|
||||||
|
// reset
|
||||||
|
options = make(map[string]*Option)
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
err := parseAndSetConfig(`
|
err := parseAndSetConfig(`
|
||||||
{
|
{
|
||||||
|
|
293
config/layers.go
293
config/layers.go
|
@ -1,293 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
configLock sync.RWMutex
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
// ErrInvalidOptionType is returned by SetConfigOption and SetDefaultConfigOption if given an unsupported option type.
|
|
||||||
ErrInvalidOptionType = errors.New("invalid option value type")
|
|
||||||
|
|
||||||
changedSignal = make(chan struct{})
|
|
||||||
)
|
|
||||||
|
|
||||||
// Changed signals if any config option was changed.
|
|
||||||
func Changed() <-chan struct{} {
|
|
||||||
configLock.RLock()
|
|
||||||
defer configLock.RUnlock()
|
|
||||||
return changedSignal
|
|
||||||
}
|
|
||||||
|
|
||||||
// triggerChange signals listeners that a config option was changed.
|
|
||||||
func triggerChange() {
|
|
||||||
// must be locked!
|
|
||||||
close(changedSignal)
|
|
||||||
changedSignal = make(chan struct{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// setConfig sets the (prioritized) user defined config.
|
|
||||||
func setConfig(m map[string]interface{}) error {
|
|
||||||
configLock.Lock()
|
|
||||||
defer configLock.Unlock()
|
|
||||||
userConfig = m
|
|
||||||
resetValidityFlag()
|
|
||||||
|
|
||||||
go pushFullUpdate()
|
|
||||||
triggerChange()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDefaultConfig sets the (fallback) default config.
|
|
||||||
func SetDefaultConfig(m map[string]interface{}) error {
|
|
||||||
configLock.Lock()
|
|
||||||
defer configLock.Unlock()
|
|
||||||
defaultConfig = m
|
|
||||||
resetValidityFlag()
|
|
||||||
|
|
||||||
go pushFullUpdate()
|
|
||||||
triggerChange()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateValue(name string, value interface{}) (*Option, error) {
|
|
||||||
optionsLock.RLock()
|
|
||||||
defer optionsLock.RUnlock()
|
|
||||||
|
|
||||||
option, ok := options[name]
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("config option does not exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := value.(type) {
|
|
||||||
case string:
|
|
||||||
if option.OptType != OptTypeString {
|
|
||||||
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 nil, fmt.Errorf("validation failed: string \"%s\" did not match regex for option %s", v, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return option, nil
|
|
||||||
case []string:
|
|
||||||
if option.OptType != OptTypeStringArray {
|
|
||||||
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 nil, fmt.Errorf("validation failed: string \"%s\" at index %d did not match regex for option %s", entry, pos, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return option, nil
|
|
||||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
|
||||||
if option.OptType != OptTypeInt {
|
|
||||||
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(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 nil, fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), name, v)
|
|
||||||
}
|
|
||||||
return option, nil
|
|
||||||
default:
|
|
||||||
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
|
|
||||||
|
|
||||||
if value == nil {
|
|
||||||
delete(userConfig, name)
|
|
||||||
} else {
|
|
||||||
var option *Option
|
|
||||||
option, err = validateValue(name, value)
|
|
||||||
if err == nil {
|
|
||||||
userConfig[name] = value
|
|
||||||
if push {
|
|
||||||
go pushUpdate(option)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
resetValidityFlag()
|
|
||||||
go saveConfig() //nolint:errcheck // error is logged
|
|
||||||
triggerChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
if value == nil {
|
|
||||||
delete(defaultConfig, name)
|
|
||||||
} else {
|
|
||||||
var option *Option
|
|
||||||
option, err = validateValue(name, value)
|
|
||||||
if err == nil {
|
|
||||||
defaultConfig[name] = value
|
|
||||||
if push {
|
|
||||||
go pushUpdate(option)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
resetValidityFlag()
|
|
||||||
triggerChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// findValue find the correct value in the user or default config.
|
|
||||||
func findValue(name string) (result interface{}) {
|
|
||||||
configLock.RLock()
|
|
||||||
defer configLock.RUnlock()
|
|
||||||
|
|
||||||
result, ok := userConfig[name]
|
|
||||||
if ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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 == nil {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
v, ok := result.(string)
|
|
||||||
if ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
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 == nil {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new
|
|
||||||
}
|
|
||||||
|
|
||||||
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 == nil {
|
|
||||||
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 fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
// findBoolValue validates and returns the value with the given name.
|
|
||||||
func findBoolValue(name string, fallback bool) (value bool) {
|
|
||||||
result := findValue(name)
|
|
||||||
if result == nil {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
v, ok := result.(bool)
|
|
||||||
if ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return fallback
|
|
||||||
}
|
|
|
@ -30,6 +30,7 @@ func prep() error {
|
||||||
if dataRoot == nil {
|
if dataRoot == nil {
|
||||||
return errors.New("data root is not set")
|
return errors.New("data root is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,14 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
|
|
||||||
"github.com/safing/portbase/database/record"
|
"github.com/safing/portbase/database/record"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Variable Type IDs for frontend Identification. Use ExternalOptType for extended types in the frontend.
|
// Various attribute options. Use ExternalOptType for extended types in the frontend.
|
||||||
const (
|
const (
|
||||||
OptTypeString uint8 = 1
|
OptTypeString uint8 = 1
|
||||||
OptTypeStringArray uint8 = 2
|
OptTypeStringArray uint8 = 2
|
||||||
|
@ -20,6 +21,10 @@ const (
|
||||||
ExpertiseLevelUser uint8 = 1
|
ExpertiseLevelUser uint8 = 1
|
||||||
ExpertiseLevelExpert uint8 = 2
|
ExpertiseLevelExpert uint8 = 2
|
||||||
ExpertiseLevelDeveloper uint8 = 3
|
ExpertiseLevelDeveloper uint8 = 3
|
||||||
|
|
||||||
|
ReleaseLevelStable = "stable"
|
||||||
|
ReleaseLevelBeta = "beta"
|
||||||
|
ReleaseLevelExperimental = "experimental"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getTypeName(t uint8) string {
|
func getTypeName(t uint8) string {
|
||||||
|
@ -39,48 +44,52 @@ func getTypeName(t uint8) string {
|
||||||
|
|
||||||
// Option describes a configuration option.
|
// Option describes a configuration option.
|
||||||
type Option struct {
|
type Option struct {
|
||||||
|
sync.Mutex
|
||||||
|
|
||||||
Name string
|
Name string
|
||||||
Key string // in path format: category/sub/key
|
Key string // in path format: category/sub/key
|
||||||
Description string
|
Description string
|
||||||
|
|
||||||
ExpertiseLevel uint8
|
ReleaseLevel string
|
||||||
OptType uint8
|
ExpertiseLevel uint8
|
||||||
|
OptType uint8
|
||||||
|
|
||||||
RequiresRestart bool
|
RequiresRestart bool
|
||||||
DefaultValue interface{}
|
DefaultValue interface{}
|
||||||
|
|
||||||
ExternalOptType string
|
ExternalOptType string
|
||||||
ValidationRegex string
|
ValidationRegex string
|
||||||
|
|
||||||
compiledRegex *regexp.Regexp
|
activeValue interface{} // runtime value (loaded from config file or set by user)
|
||||||
|
activeDefaultValue interface{} // runtime default value (may be set internally)
|
||||||
|
compiledRegex *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export expors an option to a Record.
|
// Export expors an option to a Record.
|
||||||
func (opt *Option) Export() (record.Record, error) {
|
func (option *Option) Export() (record.Record, error) {
|
||||||
data, err := json.Marshal(opt)
|
option.Lock()
|
||||||
|
defer option.Unlock()
|
||||||
|
|
||||||
|
data, err := json.Marshal(option)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
configLock.RLock()
|
if option.activeValue != nil {
|
||||||
defer configLock.RUnlock()
|
data, err = sjson.SetBytes(data, "Value", option.activeValue)
|
||||||
|
|
||||||
userValue, ok := userConfig[opt.Key]
|
|
||||||
if ok {
|
|
||||||
data, err = sjson.SetBytes(data, "Value", userValue)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultValue, ok := defaultConfig[opt.Key]
|
if option.activeDefaultValue != nil {
|
||||||
if ok {
|
data, err = sjson.SetBytes(data, "DefaultValue", option.activeDefaultValue)
|
||||||
data, err = sjson.SetBytes(data, "DefaultValue", defaultValue)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := record.NewWrapper(fmt.Sprintf("config:%s", opt.Key), nil, record.JSON, data)
|
r, err := record.NewWrapper(fmt.Sprintf("config:%s", option.Key), nil, record.JSON, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,30 +14,54 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadConfig() error {
|
func loadConfig() error {
|
||||||
|
// check if persistence is configured
|
||||||
|
if configFilePath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// read config file
|
||||||
data, err := ioutil.ReadFile(configFilePath)
|
data, err := ioutil.ReadFile(configFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := JSONToMap(data)
|
// convert to map
|
||||||
|
newValues, err := JSONToMap(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return setConfig(m)
|
// apply
|
||||||
|
return setConfig(newValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveConfig() (err error) {
|
func saveConfig() error {
|
||||||
data, err := MapToJSON(userConfig)
|
// check if persistence is configured
|
||||||
if err == nil {
|
if configFilePath == "" {
|
||||||
err = ioutil.WriteFile(configFilePath, data, 0600)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extract values
|
||||||
|
activeValues := make(map[string]interface{})
|
||||||
|
optionsLock.RLock()
|
||||||
|
for key, option := range options {
|
||||||
|
option.Lock()
|
||||||
|
if option.activeValue != nil {
|
||||||
|
activeValues[key] = option.activeValue
|
||||||
|
}
|
||||||
|
option.Unlock()
|
||||||
|
}
|
||||||
|
optionsLock.RUnlock()
|
||||||
|
|
||||||
|
// convert to JSON
|
||||||
|
data, err := MapToJSON(activeValues)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("config: failed to save config: %s", err)
|
log.Errorf("config: failed to save config: %s", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
// write file
|
||||||
|
return ioutil.WriteFile(configFilePath, data, 0600)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONToMap parses and flattens a hierarchical json object.
|
// JSONToMap parses and flattens a hierarchical json object.
|
||||||
|
@ -73,18 +97,10 @@ func flatten(rootMap, subMap map[string]interface{}, subKey string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MapToJSON expands a flattened map and returns it as json.
|
// MapToJSON expands a flattened map and returns it as json. The map is altered in the process.
|
||||||
func MapToJSON(mapData map[string]interface{}) ([]byte, error) {
|
func MapToJSON(values map[string]interface{}) ([]byte, error) {
|
||||||
configLock.RLock()
|
expand(values)
|
||||||
defer configLock.RUnlock()
|
return json.MarshalIndent(values, "", " ")
|
||||||
|
|
||||||
new := make(map[string]interface{})
|
|
||||||
for key, value := range mapData {
|
|
||||||
new[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
expand(new)
|
|
||||||
return json.MarshalIndent(new, "", " ")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// expand expands a flattened map.
|
// expand expands a flattened map.
|
||||||
|
|
|
@ -21,8 +21,9 @@ func Register(option *Option) error {
|
||||||
if option.Name == "" ||
|
if option.Name == "" ||
|
||||||
option.Key == "" ||
|
option.Key == "" ||
|
||||||
option.Description == "" ||
|
option.Description == "" ||
|
||||||
|
option.OptType == 0 ||
|
||||||
option.ExpertiseLevel == 0 ||
|
option.ExpertiseLevel == 0 ||
|
||||||
option.OptType == 0 {
|
option.ReleaseLevel == "" {
|
||||||
return ErrIncompleteCall
|
return ErrIncompleteCall
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRegistry(t *testing.T) {
|
func TestRegistry(t *testing.T) {
|
||||||
|
// reset
|
||||||
|
options = make(map[string]*Option)
|
||||||
|
|
||||||
if err := Register(&Option{
|
if err := Register(&Option{
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Key: "key",
|
Key: "key",
|
||||||
Description: "description",
|
Description: "description",
|
||||||
ExpertiseLevel: 1,
|
ReleaseLevel: ReleaseLevelStable,
|
||||||
|
ExpertiseLevel: ExpertiseLevelUser,
|
||||||
OptType: OptTypeString,
|
OptType: OptTypeString,
|
||||||
DefaultValue: "default",
|
DefaultValue: "default",
|
||||||
ValidationRegex: "^(banana|water)$",
|
ValidationRegex: "^(banana|water)$",
|
||||||
|
@ -22,7 +25,8 @@ func TestRegistry(t *testing.T) {
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Key: "key",
|
Key: "key",
|
||||||
Description: "description",
|
Description: "description",
|
||||||
ExpertiseLevel: 1,
|
ReleaseLevel: ReleaseLevelStable,
|
||||||
|
ExpertiseLevel: ExpertiseLevelUser,
|
||||||
OptType: 0,
|
OptType: 0,
|
||||||
DefaultValue: "default",
|
DefaultValue: "default",
|
||||||
ValidationRegex: "^[A-Z][a-z]+$",
|
ValidationRegex: "^[A-Z][a-z]+$",
|
||||||
|
@ -34,7 +38,8 @@ func TestRegistry(t *testing.T) {
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Key: "key",
|
Key: "key",
|
||||||
Description: "description",
|
Description: "description",
|
||||||
ExpertiseLevel: 1,
|
ReleaseLevel: ReleaseLevelStable,
|
||||||
|
ExpertiseLevel: ExpertiseLevelUser,
|
||||||
OptType: OptTypeString,
|
OptType: OptTypeString,
|
||||||
DefaultValue: "default",
|
DefaultValue: "default",
|
||||||
ValidationRegex: "[",
|
ValidationRegex: "[",
|
||||||
|
|
57
config/release.go
Normal file
57
config/release.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
releaseLevelKey = "core/release_level"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
releaseLevel = ReleaseLevelStable
|
||||||
|
releaseLevelLock sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registerReleaseLevelOption()
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerReleaseLevelOption() {
|
||||||
|
err := Register(&Option{
|
||||||
|
Name: "Release Selection",
|
||||||
|
Key: releaseLevelKey,
|
||||||
|
Description: "Select maturity level of features that should be available",
|
||||||
|
|
||||||
|
OptType: OptTypeString,
|
||||||
|
ExpertiseLevel: ExpertiseLevelExpert,
|
||||||
|
ReleaseLevel: ReleaseLevelStable,
|
||||||
|
|
||||||
|
RequiresRestart: false,
|
||||||
|
DefaultValue: ReleaseLevelStable,
|
||||||
|
|
||||||
|
ExternalOptType: "string list",
|
||||||
|
ValidationRegex: fmt.Sprintf("^(%s|%s|%s)$", ReleaseLevelStable, ReleaseLevelBeta, ReleaseLevelExperimental),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateReleaseLevel() {
|
||||||
|
new := findStringValue(releaseLevelKey, "")
|
||||||
|
releaseLevelLock.Lock()
|
||||||
|
if new == "" {
|
||||||
|
releaseLevel = ReleaseLevelStable
|
||||||
|
} else {
|
||||||
|
releaseLevel = new
|
||||||
|
}
|
||||||
|
releaseLevelLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getReleaseLevel() string {
|
||||||
|
releaseLevelLock.Lock()
|
||||||
|
defer releaseLevelLock.Unlock()
|
||||||
|
return releaseLevel
|
||||||
|
}
|
207
config/set.go
Normal file
207
config/set.go
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/tevino/abool"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidJSON is returned by SetConfig and SetDefaultConfig if they receive invalid json.
|
||||||
|
ErrInvalidJSON = errors.New("json string invalid")
|
||||||
|
|
||||||
|
// ErrInvalidOptionType is returned by SetConfigOption and SetDefaultConfigOption if given an unsupported option type.
|
||||||
|
ErrInvalidOptionType = errors.New("invalid option value type")
|
||||||
|
|
||||||
|
validityFlag = abool.NewBool(true)
|
||||||
|
validityFlagLock sync.RWMutex
|
||||||
|
|
||||||
|
changedSignal = make(chan struct{})
|
||||||
|
changedSignalLock sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func getValidityFlag() *abool.AtomicBool {
|
||||||
|
validityFlagLock.RLock()
|
||||||
|
defer validityFlagLock.RUnlock()
|
||||||
|
return validityFlag
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changed signals if any config option was changed.
|
||||||
|
func Changed() <-chan struct{} {
|
||||||
|
changedSignalLock.Lock()
|
||||||
|
defer changedSignalLock.Unlock()
|
||||||
|
return changedSignal
|
||||||
|
}
|
||||||
|
|
||||||
|
func signalChanges() {
|
||||||
|
// refetch and save release level
|
||||||
|
updateReleaseLevel()
|
||||||
|
|
||||||
|
// reset validity flag
|
||||||
|
validityFlagLock.Lock()
|
||||||
|
validityFlag.SetTo(false)
|
||||||
|
validityFlag = abool.NewBool(true)
|
||||||
|
validityFlagLock.Unlock()
|
||||||
|
|
||||||
|
// trigger change signal: signal listeners that a config option was changed.
|
||||||
|
changedSignalLock.Lock()
|
||||||
|
close(changedSignal)
|
||||||
|
changedSignal = make(chan struct{})
|
||||||
|
changedSignalLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// setConfig sets the (prioritized) user defined config.
|
||||||
|
func setConfig(newValues map[string]interface{}) error {
|
||||||
|
optionsLock.Lock()
|
||||||
|
for key, option := range options {
|
||||||
|
newValue, ok := newValues[key]
|
||||||
|
option.Lock()
|
||||||
|
if ok {
|
||||||
|
option.activeValue = newValue
|
||||||
|
} else {
|
||||||
|
option.activeValue = nil
|
||||||
|
}
|
||||||
|
option.Unlock()
|
||||||
|
}
|
||||||
|
optionsLock.Unlock()
|
||||||
|
|
||||||
|
signalChanges()
|
||||||
|
go pushFullUpdate()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaultConfig sets the (fallback) default config.
|
||||||
|
func SetDefaultConfig(newValues map[string]interface{}) error {
|
||||||
|
optionsLock.Lock()
|
||||||
|
for key, option := range options {
|
||||||
|
newValue, ok := newValues[key]
|
||||||
|
option.Lock()
|
||||||
|
if ok {
|
||||||
|
option.activeDefaultValue = newValue
|
||||||
|
} else {
|
||||||
|
option.activeDefaultValue = nil
|
||||||
|
}
|
||||||
|
option.Unlock()
|
||||||
|
}
|
||||||
|
optionsLock.Unlock()
|
||||||
|
|
||||||
|
signalChanges()
|
||||||
|
go pushFullUpdate()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateValue(option *Option, value interface{}) error {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
if option.OptType != OptTypeString {
|
||||||
|
return fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), option.Key, 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, option.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case []string:
|
||||||
|
if option.OptType != OptTypeStringArray {
|
||||||
|
return fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), option.Key, 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, option.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||||
|
if option.OptType != OptTypeInt {
|
||||||
|
return fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), option.Key, v)
|
||||||
|
}
|
||||||
|
if option.compiledRegex != nil {
|
||||||
|
if !option.compiledRegex.MatchString(fmt.Sprintf("%d", v)) {
|
||||||
|
return fmt.Errorf("validation failed: number \"%d\" did not match regex for option %s", v, option.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case bool:
|
||||||
|
if option.OptType != OptTypeBool {
|
||||||
|
return fmt.Errorf("expected type %s for option %s, got type %T", getTypeName(option.OptType), option.Key, v)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid option value type: %T", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfigOption sets a single value in the (prioritized) user defined config.
|
||||||
|
func SetConfigOption(key string, value interface{}) error {
|
||||||
|
return setConfigOption(key, value, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setConfigOption(key string, value interface{}, push bool) (err error) {
|
||||||
|
optionsLock.Lock()
|
||||||
|
option, ok := options[key]
|
||||||
|
optionsLock.Unlock()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("config option %s does not exist", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
option.Lock()
|
||||||
|
if value == nil {
|
||||||
|
option.activeValue = nil
|
||||||
|
} else {
|
||||||
|
err = validateValue(option, value)
|
||||||
|
if err == nil {
|
||||||
|
option.activeValue = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
option.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalize change, activate triggers
|
||||||
|
signalChanges()
|
||||||
|
if push {
|
||||||
|
go pushUpdate(option)
|
||||||
|
}
|
||||||
|
return saveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaultConfigOption sets a single value in the (fallback) default config.
|
||||||
|
func SetDefaultConfigOption(key string, value interface{}) error {
|
||||||
|
return setDefaultConfigOption(key, value, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDefaultConfigOption(key string, value interface{}, push bool) (err error) {
|
||||||
|
optionsLock.Lock()
|
||||||
|
option, ok := options[key]
|
||||||
|
optionsLock.Unlock()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("config option %s does not exist", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
option.Lock()
|
||||||
|
if value == nil {
|
||||||
|
option.activeDefaultValue = nil
|
||||||
|
} else {
|
||||||
|
err = validateValue(option, value)
|
||||||
|
if err == nil {
|
||||||
|
option.activeDefaultValue = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
option.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalize change, activate triggers
|
||||||
|
signalChanges()
|
||||||
|
if push {
|
||||||
|
go pushUpdate(option)
|
||||||
|
}
|
||||||
|
return saveConfig()
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ package config
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestLayersGetters(t *testing.T) {
|
func TestLayersGetters(t *testing.T) {
|
||||||
|
// reset
|
||||||
|
options = make(map[string]*Option)
|
||||||
|
|
||||||
mapData, err := JSONToMap([]byte(`
|
mapData, err := JSONToMap([]byte(`
|
||||||
{
|
{
|
||||||
|
@ -79,12 +81,15 @@ func TestLayersGetters(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLayersSetters(t *testing.T) {
|
func TestLayersSetters(t *testing.T) {
|
||||||
|
// reset
|
||||||
|
options = make(map[string]*Option)
|
||||||
|
|
||||||
Register(&Option{
|
Register(&Option{
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Key: "monkey",
|
Key: "monkey",
|
||||||
Description: "description",
|
Description: "description",
|
||||||
ExpertiseLevel: 1,
|
ReleaseLevel: ReleaseLevelStable,
|
||||||
|
ExpertiseLevel: ExpertiseLevelUser,
|
||||||
OptType: OptTypeString,
|
OptType: OptTypeString,
|
||||||
DefaultValue: "banana",
|
DefaultValue: "banana",
|
||||||
ValidationRegex: "^(banana|water)$",
|
ValidationRegex: "^(banana|water)$",
|
||||||
|
@ -93,7 +98,8 @@ func TestLayersSetters(t *testing.T) {
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Key: "zebras/zebra",
|
Key: "zebras/zebra",
|
||||||
Description: "description",
|
Description: "description",
|
||||||
ExpertiseLevel: 1,
|
ReleaseLevel: ReleaseLevelStable,
|
||||||
|
ExpertiseLevel: ExpertiseLevelUser,
|
||||||
OptType: OptTypeStringArray,
|
OptType: OptTypeStringArray,
|
||||||
DefaultValue: []string{"black", "white"},
|
DefaultValue: []string{"black", "white"},
|
||||||
ValidationRegex: "^[a-z]+$",
|
ValidationRegex: "^[a-z]+$",
|
||||||
|
@ -102,7 +108,8 @@ func TestLayersSetters(t *testing.T) {
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Key: "elephant",
|
Key: "elephant",
|
||||||
Description: "description",
|
Description: "description",
|
||||||
ExpertiseLevel: 1,
|
ReleaseLevel: ReleaseLevelStable,
|
||||||
|
ExpertiseLevel: ExpertiseLevelUser,
|
||||||
OptType: OptTypeInt,
|
OptType: OptTypeInt,
|
||||||
DefaultValue: 2,
|
DefaultValue: 2,
|
||||||
ValidationRegex: "",
|
ValidationRegex: "",
|
||||||
|
@ -111,7 +118,8 @@ func TestLayersSetters(t *testing.T) {
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Key: "hot",
|
Key: "hot",
|
||||||
Description: "description",
|
Description: "description",
|
||||||
ExpertiseLevel: 1,
|
ReleaseLevel: ReleaseLevelStable,
|
||||||
|
ExpertiseLevel: ExpertiseLevelUser,
|
||||||
OptType: OptTypeBool,
|
OptType: OptTypeBool,
|
||||||
DefaultValue: true,
|
DefaultValue: true,
|
||||||
ValidationRegex: "",
|
ValidationRegex: "",
|
||||||
|
@ -180,8 +188,8 @@ func TestLayersSetters(t *testing.T) {
|
||||||
if err := SetDefaultConfigOption("elephant", nil); err != nil {
|
if err := SetDefaultConfigOption("elephant", nil); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
if err := SetDefaultConfigOption("invalid_delete", nil); err != nil {
|
if err := SetDefaultConfigOption("invalid_delete", nil); err == nil {
|
||||||
t.Error(err)
|
t.Error("should fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue