Merge pull request #11 from safing/feature/config-release-levels

Add release level attribute to config option
This commit is contained in:
Daniel 2019-09-24 14:44:30 +02:00 committed by GitHub
commit 23bfe9ecd9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 622 additions and 371 deletions

View file

@ -37,8 +37,9 @@ func registerConfig() error {
Name: "API Address",
Key: "api/listenAddress",
Description: "Define on which IP and port the API should listen on.",
ExpertiseLevel: config.ExpertiseLevelDeveloper,
OptType: config.OptTypeString,
ExpertiseLevel: config.ExpertiseLevelDeveloper,
ReleaseLevel: config.ReleaseLevelStable,
DefaultValue: getDefaultListenAddress(),
ValidationRegex: "^([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}:[0-9]{1,5}|\\[[:0-9A-Fa-f]+\\]:[0-9]{1,5})$",
RequiresRestart: true,
@ -53,5 +54,6 @@ func registerConfig() error {
// SetDefaultAPIListenAddress sets the default listen address for the API.
func SetDefaultAPIListenAddress(address string) {
defaultListenAddress = address
}

View file

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

View file

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

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 {
return errors.New("data root is not set")
}
return nil
}

View file

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

View file

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

View file

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

View file

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

View file

@ -31,8 +31,9 @@ func prep() error {
Name: "RNG Cipher",
Key: "random/rng_cipher",
Description: "Cipher to use for the Fortuna RNG. Requires restart to take effect.",
ExpertiseLevel: config.ExpertiseLevelDeveloper,
OptType: config.OptTypeString,
ExpertiseLevel: config.ExpertiseLevelDeveloper,
ReleaseLevel: config.ReleaseLevelExperimental,
ExternalOptType: "string list",
DefaultValue: "aes",
ValidationRegex: "^(aes|serpent)$",
@ -46,8 +47,9 @@ func prep() error {
Name: "Minimum Feed Entropy",
Key: "random/min_feed_entropy",
Description: "The minimum amount of entropy before a entropy source is feed to the RNG, in bits.",
ExpertiseLevel: config.ExpertiseLevelDeveloper,
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelDeveloper,
ReleaseLevel: config.ReleaseLevelExperimental,
DefaultValue: 256,
ValidationRegex: "^[0-9]{3,5}$",
})
@ -60,8 +62,9 @@ func prep() error {
Name: "Reseed after x seconds",
Key: "random/reseed_after_seconds",
Description: "Number of seconds until reseed",
ExpertiseLevel: config.ExpertiseLevelDeveloper,
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelDeveloper,
ReleaseLevel: config.ReleaseLevelExperimental,
DefaultValue: 360, // ten minutes
ValidationRegex: "^[1-9][0-9]{1,5}$",
})
@ -74,8 +77,9 @@ func prep() error {
Name: "Reseed after x bytes",
Key: "random/reseed_after_bytes",
Description: "Number of fetched bytes until reseed",
ExpertiseLevel: config.ExpertiseLevelDeveloper,
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelDeveloper,
ReleaseLevel: config.ReleaseLevelExperimental,
DefaultValue: 1000000, // one megabyte
ValidationRegex: "^[1-9][0-9]{2,9}$",
})