Add release level attribute to config option, improve internal handling

This commit is contained in:
Daniel 2019-09-24 00:01:58 +02:00
parent fad816ca71
commit 2a1681792d
11 changed files with 611 additions and 366 deletions

View file

@ -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
}

View file

@ -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(`
{ {

View file

@ -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
}

View file

@ -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
} }

View file

@ -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
} }

View file

@ -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.

View file

@ -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
} }

View file

@ -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
View 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
View 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()
}

View file

@ -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")
} }
} }