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
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
)
|
||||
|
||||
var (
|
||||
validityFlag = abool.NewBool(true)
|
||||
validityFlagLock sync.RWMutex
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -22,19 +15,6 @@ type (
|
|||
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.
|
||||
func GetAsString(name string, fallback string) StringOption {
|
||||
valid := getValidityFlag()
|
||||
|
@ -86,3 +66,127 @@ func GetAsBool(name string, fallback bool) BoolOption {
|
|||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
// reset
|
||||
options = make(map[string]*Option)
|
||||
|
||||
err := log.Start()
|
||||
if err != nil {
|
||||
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(`
|
||||
{
|
||||
"monkey": "1",
|
||||
|
@ -59,27 +82,27 @@ func TestGet(t *testing.T) {
|
|||
|
||||
monkey := GetAsString("monkey", "none")
|
||||
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{})
|
||||
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)
|
||||
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)
|
||||
if !hot() {
|
||||
t.Fatalf("hot should be true, is %v", hot())
|
||||
t.Errorf("hot should be true, is %v", hot())
|
||||
}
|
||||
|
||||
cold := GetAsBool("cold", true)
|
||||
if cold() {
|
||||
t.Fatalf("cold should be false, is %v", cold())
|
||||
t.Errorf("cold should be false, is %v", cold())
|
||||
}
|
||||
|
||||
err = parseAndSetConfig(`
|
||||
|
@ -92,19 +115,126 @@ func TestGet(t *testing.T) {
|
|||
}
|
||||
|
||||
if monkey() != "3" {
|
||||
t.Fatalf("monkey should be 0, is %s", monkey())
|
||||
t.Errorf("monkey should be 0, is %s", monkey())
|
||||
}
|
||||
|
||||
if elephant() != 0 {
|
||||
t.Fatalf("elephant should be 0, is %d", elephant())
|
||||
t.Errorf("elephant should be 0, is %d", elephant())
|
||||
}
|
||||
|
||||
zebra()
|
||||
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) {
|
||||
// reset
|
||||
options = make(map[string]*Option)
|
||||
|
||||
// Setup
|
||||
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 {
|
||||
return errors.New("data root is not set")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -4,13 +4,14 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sync"
|
||||
|
||||
"github.com/tidwall/sjson"
|
||||
|
||||
"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 (
|
||||
OptTypeString uint8 = 1
|
||||
OptTypeStringArray uint8 = 2
|
||||
|
@ -20,6 +21,10 @@ const (
|
|||
ExpertiseLevelUser uint8 = 1
|
||||
ExpertiseLevelExpert uint8 = 2
|
||||
ExpertiseLevelDeveloper uint8 = 3
|
||||
|
||||
ReleaseLevelStable = "stable"
|
||||
ReleaseLevelBeta = "beta"
|
||||
ReleaseLevelExperimental = "experimental"
|
||||
)
|
||||
|
||||
func getTypeName(t uint8) string {
|
||||
|
@ -39,48 +44,52 @@ func getTypeName(t uint8) string {
|
|||
|
||||
// Option describes a configuration option.
|
||||
type Option struct {
|
||||
sync.Mutex
|
||||
|
||||
Name string
|
||||
Key string // in path format: category/sub/key
|
||||
Description string
|
||||
|
||||
ExpertiseLevel uint8
|
||||
OptType uint8
|
||||
ReleaseLevel string
|
||||
ExpertiseLevel uint8
|
||||
OptType uint8
|
||||
|
||||
RequiresRestart bool
|
||||
DefaultValue interface{}
|
||||
|
||||
ExternalOptType 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.
|
||||
func (opt *Option) Export() (record.Record, error) {
|
||||
data, err := json.Marshal(opt)
|
||||
func (option *Option) Export() (record.Record, error) {
|
||||
option.Lock()
|
||||
defer option.Unlock()
|
||||
|
||||
data, err := json.Marshal(option)
|
||||
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 option.activeValue != nil {
|
||||
data, err = sjson.SetBytes(data, "Value", option.activeValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
defaultValue, ok := defaultConfig[opt.Key]
|
||||
if ok {
|
||||
data, err = sjson.SetBytes(data, "DefaultValue", defaultValue)
|
||||
if option.activeDefaultValue != nil {
|
||||
data, err = sjson.SetBytes(data, "DefaultValue", option.activeDefaultValue)
|
||||
if err != nil {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -14,30 +14,54 @@ var (
|
|||
)
|
||||
|
||||
func loadConfig() error {
|
||||
// check if persistence is configured
|
||||
if configFilePath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// read config file
|
||||
data, err := ioutil.ReadFile(configFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m, err := JSONToMap(data)
|
||||
// convert to map
|
||||
newValues, err := JSONToMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return setConfig(m)
|
||||
// apply
|
||||
return setConfig(newValues)
|
||||
}
|
||||
|
||||
func saveConfig() (err error) {
|
||||
data, err := MapToJSON(userConfig)
|
||||
if err == nil {
|
||||
err = ioutil.WriteFile(configFilePath, data, 0600)
|
||||
func saveConfig() error {
|
||||
// check if persistence is configured
|
||||
if configFilePath == "" {
|
||||
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 {
|
||||
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.
|
||||
|
@ -73,18 +97,10 @@ func flatten(rootMap, subMap map[string]interface{}, subKey string) {
|
|||
}
|
||||
}
|
||||
|
||||
// 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, "", " ")
|
||||
// MapToJSON expands a flattened map and returns it as json. The map is altered in the process.
|
||||
func MapToJSON(values map[string]interface{}) ([]byte, error) {
|
||||
expand(values)
|
||||
return json.MarshalIndent(values, "", " ")
|
||||
}
|
||||
|
||||
// expand expands a flattened map.
|
||||
|
|
|
@ -21,8 +21,9 @@ func Register(option *Option) error {
|
|||
if option.Name == "" ||
|
||||
option.Key == "" ||
|
||||
option.Description == "" ||
|
||||
option.OptType == 0 ||
|
||||
option.ExpertiseLevel == 0 ||
|
||||
option.OptType == 0 {
|
||||
option.ReleaseLevel == "" {
|
||||
return ErrIncompleteCall
|
||||
}
|
||||
|
||||
|
|
|
@ -5,12 +5,15 @@ import (
|
|||
)
|
||||
|
||||
func TestRegistry(t *testing.T) {
|
||||
// reset
|
||||
options = make(map[string]*Option)
|
||||
|
||||
if err := Register(&Option{
|
||||
Name: "name",
|
||||
Key: "key",
|
||||
Description: "description",
|
||||
ExpertiseLevel: 1,
|
||||
ReleaseLevel: ReleaseLevelStable,
|
||||
ExpertiseLevel: ExpertiseLevelUser,
|
||||
OptType: OptTypeString,
|
||||
DefaultValue: "default",
|
||||
ValidationRegex: "^(banana|water)$",
|
||||
|
@ -22,7 +25,8 @@ func TestRegistry(t *testing.T) {
|
|||
Name: "name",
|
||||
Key: "key",
|
||||
Description: "description",
|
||||
ExpertiseLevel: 1,
|
||||
ReleaseLevel: ReleaseLevelStable,
|
||||
ExpertiseLevel: ExpertiseLevelUser,
|
||||
OptType: 0,
|
||||
DefaultValue: "default",
|
||||
ValidationRegex: "^[A-Z][a-z]+$",
|
||||
|
@ -34,7 +38,8 @@ func TestRegistry(t *testing.T) {
|
|||
Name: "name",
|
||||
Key: "key",
|
||||
Description: "description",
|
||||
ExpertiseLevel: 1,
|
||||
ReleaseLevel: ReleaseLevelStable,
|
||||
ExpertiseLevel: ExpertiseLevelUser,
|
||||
OptType: OptTypeString,
|
||||
DefaultValue: "default",
|
||||
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"
|
||||
|
||||
func TestLayersGetters(t *testing.T) {
|
||||
// reset
|
||||
options = make(map[string]*Option)
|
||||
|
||||
mapData, err := JSONToMap([]byte(`
|
||||
{
|
||||
|
@ -79,12 +81,15 @@ func TestLayersGetters(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLayersSetters(t *testing.T) {
|
||||
// reset
|
||||
options = make(map[string]*Option)
|
||||
|
||||
Register(&Option{
|
||||
Name: "name",
|
||||
Key: "monkey",
|
||||
Description: "description",
|
||||
ExpertiseLevel: 1,
|
||||
ReleaseLevel: ReleaseLevelStable,
|
||||
ExpertiseLevel: ExpertiseLevelUser,
|
||||
OptType: OptTypeString,
|
||||
DefaultValue: "banana",
|
||||
ValidationRegex: "^(banana|water)$",
|
||||
|
@ -93,7 +98,8 @@ func TestLayersSetters(t *testing.T) {
|
|||
Name: "name",
|
||||
Key: "zebras/zebra",
|
||||
Description: "description",
|
||||
ExpertiseLevel: 1,
|
||||
ReleaseLevel: ReleaseLevelStable,
|
||||
ExpertiseLevel: ExpertiseLevelUser,
|
||||
OptType: OptTypeStringArray,
|
||||
DefaultValue: []string{"black", "white"},
|
||||
ValidationRegex: "^[a-z]+$",
|
||||
|
@ -102,7 +108,8 @@ func TestLayersSetters(t *testing.T) {
|
|||
Name: "name",
|
||||
Key: "elephant",
|
||||
Description: "description",
|
||||
ExpertiseLevel: 1,
|
||||
ReleaseLevel: ReleaseLevelStable,
|
||||
ExpertiseLevel: ExpertiseLevelUser,
|
||||
OptType: OptTypeInt,
|
||||
DefaultValue: 2,
|
||||
ValidationRegex: "",
|
||||
|
@ -111,7 +118,8 @@ func TestLayersSetters(t *testing.T) {
|
|||
Name: "name",
|
||||
Key: "hot",
|
||||
Description: "description",
|
||||
ExpertiseLevel: 1,
|
||||
ReleaseLevel: ReleaseLevelStable,
|
||||
ExpertiseLevel: ExpertiseLevelUser,
|
||||
OptType: OptTypeBool,
|
||||
DefaultValue: true,
|
||||
ValidationRegex: "",
|
||||
|
@ -180,8 +188,8 @@ func TestLayersSetters(t *testing.T) {
|
|||
if err := SetDefaultConfigOption("elephant", nil); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := SetDefaultConfigOption("invalid_delete", nil); err != nil {
|
||||
t.Error(err)
|
||||
if err := SetDefaultConfigOption("invalid_delete", nil); err == nil {
|
||||
t.Error("should fail")
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue