mirror of
https://github.com/safing/portbase
synced 2025-09-02 18:50:14 +00:00
Merge pull request #72 from safing/feature/options
Extend config options
This commit is contained in:
commit
6c82df5523
14 changed files with 446 additions and 145 deletions
|
@ -42,13 +42,15 @@ func registerConfig() error {
|
||||||
Name: "API Address",
|
Name: "API Address",
|
||||||
Key: CfgDefaultListenAddressKey,
|
Key: CfgDefaultListenAddressKey,
|
||||||
Description: "Define on which IP and port the API should listen on.",
|
Description: "Define on which IP and port the API should listen on.",
|
||||||
Order: 128,
|
|
||||||
OptType: config.OptTypeString,
|
OptType: config.OptTypeString,
|
||||||
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
||||||
ReleaseLevel: config.ReleaseLevelStable,
|
ReleaseLevel: config.ReleaseLevelStable,
|
||||||
DefaultValue: getDefaultListenAddress(),
|
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})$",
|
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,
|
RequiresRestart: true,
|
||||||
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayOrderAnnotation: 128,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -110,7 +110,7 @@ func (s *StorageInterface) Query(q *query.Query, local, internal bool) (*iterato
|
||||||
|
|
||||||
func (s *StorageInterface) processQuery(it *iterator.Iterator, opts []*Option) {
|
func (s *StorageInterface) processQuery(it *iterator.Iterator, opts []*Option) {
|
||||||
|
|
||||||
sort.Sort(sortableOptions(opts))
|
sort.Sort(sortByKey(opts))
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
r, err := opt.Export()
|
r, err := opt.Export()
|
||||||
|
|
|
@ -3,17 +3,22 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/tevino/abool"
|
"github.com/tevino/abool"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ExpertiseLevel allows to group settings by user expertise.
|
||||||
|
// It's useful if complex or technical settings should be hidden
|
||||||
|
// from the average user while still allowing experts and developers
|
||||||
|
// to change deep configuration settings.
|
||||||
|
type ExpertiseLevel uint8
|
||||||
|
|
||||||
// Expertise Level constants
|
// Expertise Level constants
|
||||||
const (
|
const (
|
||||||
ExpertiseLevelUser uint8 = 0
|
ExpertiseLevelUser ExpertiseLevel = 0
|
||||||
ExpertiseLevelExpert uint8 = 1
|
ExpertiseLevelExpert ExpertiseLevel = 1
|
||||||
ExpertiseLevelDeveloper uint8 = 2
|
ExpertiseLevelDeveloper ExpertiseLevel = 2
|
||||||
|
|
||||||
ExpertiseLevelNameUser = "user"
|
ExpertiseLevelNameUser = "user"
|
||||||
ExpertiseLevelNameExpert = "expert"
|
ExpertiseLevelNameExpert = "expert"
|
||||||
|
@ -23,15 +28,12 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
expertiseLevel *int32
|
|
||||||
expertiseLevelOption *Option
|
expertiseLevelOption *Option
|
||||||
|
expertiseLevel = new(int32)
|
||||||
expertiseLevelOptionFlag = abool.New()
|
expertiseLevelOptionFlag = abool.New()
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
var expertiseLevelVal int32
|
|
||||||
expertiseLevel = &expertiseLevelVal
|
|
||||||
|
|
||||||
registerExpertiseLevelOption()
|
registerExpertiseLevelOption()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,16 +42,30 @@ func registerExpertiseLevelOption() {
|
||||||
Name: "Expertise Level",
|
Name: "Expertise Level",
|
||||||
Key: expertiseLevelKey,
|
Key: expertiseLevelKey,
|
||||||
Description: "The Expertise Level controls the perceived complexity. Higher settings will show you more complex settings and information. This might also affect various other things relying on this setting. Modified settings in higher expertise levels stay in effect when switching back. (Unlike the Release Level)",
|
Description: "The Expertise Level controls the perceived complexity. Higher settings will show you more complex settings and information. This might also affect various other things relying on this setting. Modified settings in higher expertise levels stay in effect when switching back. (Unlike the Release Level)",
|
||||||
|
|
||||||
OptType: OptTypeString,
|
OptType: OptTypeString,
|
||||||
ExpertiseLevel: ExpertiseLevelUser,
|
ExpertiseLevel: ExpertiseLevelUser,
|
||||||
ReleaseLevel: ExpertiseLevelUser,
|
ReleaseLevel: ReleaseLevelStable,
|
||||||
|
|
||||||
RequiresRestart: false,
|
|
||||||
DefaultValue: ExpertiseLevelNameUser,
|
DefaultValue: ExpertiseLevelNameUser,
|
||||||
|
Annotations: Annotations{
|
||||||
ExternalOptType: "string list",
|
DisplayHintAnnotation: DisplayHintOneOf,
|
||||||
ValidationRegex: fmt.Sprintf("^(%s|%s|%s)$", ExpertiseLevelNameUser, ExpertiseLevelNameExpert, ExpertiseLevelNameDeveloper),
|
},
|
||||||
|
PossibleValues: []PossibleValue{
|
||||||
|
{
|
||||||
|
Name: "Easy",
|
||||||
|
Value: ExpertiseLevelNameUser,
|
||||||
|
Description: "Easy application mode by hidding complex settings.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Expert",
|
||||||
|
Value: ExpertiseLevelNameExpert,
|
||||||
|
Description: "Expert application mode. Allows access to almost all configuration options.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Developer",
|
||||||
|
Value: ExpertiseLevelNameDeveloper,
|
||||||
|
Description: "Developer mode. Please be careful!",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := Register(expertiseLevelOption)
|
err := Register(expertiseLevelOption)
|
||||||
|
|
|
@ -15,7 +15,7 @@ type (
|
||||||
BoolOption func() bool
|
BoolOption func() bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func getValueCache(name string, option *Option, requestedType uint8) (*Option, *valueCache) {
|
func getValueCache(name string, option *Option, requestedType OptionType) (*Option, *valueCache) {
|
||||||
// get option
|
// get option
|
||||||
if option == nil {
|
if option == nil {
|
||||||
var ok bool
|
var ok bool
|
||||||
|
|
|
@ -25,7 +25,7 @@ func parseAndSetDefaultConfig(jsonData string) error {
|
||||||
return SetDefaultConfig(m)
|
return SetDefaultConfig(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func quickRegister(t *testing.T, key string, optType uint8, defaultValue interface{}) {
|
func quickRegister(t *testing.T, key string, optType OptionType, defaultValue interface{}) {
|
||||||
err := Register(&Option{
|
err := Register(&Option{
|
||||||
Name: key,
|
Name: key,
|
||||||
Key: key,
|
Key: key,
|
||||||
|
|
207
config/option.go
207
config/option.go
|
@ -11,15 +11,18 @@ import (
|
||||||
"github.com/safing/portbase/database/record"
|
"github.com/safing/portbase/database/record"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// OptionType defines the value type of an option.
|
||||||
|
type OptionType uint8
|
||||||
|
|
||||||
// Various attribute options. 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 OptionType = 1
|
||||||
OptTypeStringArray uint8 = 2
|
OptTypeStringArray OptionType = 2
|
||||||
OptTypeInt uint8 = 3
|
OptTypeInt OptionType = 3
|
||||||
OptTypeBool uint8 = 4
|
OptTypeBool OptionType = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
func getTypeName(t uint8) string {
|
func getTypeName(t OptionType) string {
|
||||||
switch t {
|
switch t {
|
||||||
case OptTypeString:
|
case OptTypeString:
|
||||||
return "string"
|
return "string"
|
||||||
|
@ -34,25 +37,140 @@ func getTypeName(t uint8) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PossibleValue defines a value that is possible for
|
||||||
|
// a configuration setting.
|
||||||
|
type PossibleValue struct {
|
||||||
|
// Name is a human readable name of the option.
|
||||||
|
Name string
|
||||||
|
// Description is a human readable description of
|
||||||
|
// this value.
|
||||||
|
Description string
|
||||||
|
// Value is the actual value of the option. The type
|
||||||
|
// must match the option's value type.
|
||||||
|
Value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Annotations can be attached to configuration options to
|
||||||
|
// provide hints for user interfaces or other systems working
|
||||||
|
// or setting configuration options.
|
||||||
|
// Annotation keys should follow the below format to ensure
|
||||||
|
// future well-known annotation additions do not conflict
|
||||||
|
// with vendor/product/package specific annoations.
|
||||||
|
//
|
||||||
|
// Format: <vendor/package>:<scope>:<identifier>
|
||||||
|
type Annotations map[string]interface{}
|
||||||
|
|
||||||
|
// Well known annotations defined by this package.
|
||||||
|
const (
|
||||||
|
// DisplayHintAnnotation provides a hint for the user
|
||||||
|
// interface on how to render an option.
|
||||||
|
// The value of DisplayHintAnnotation is expected to
|
||||||
|
// be a string. See DisplayHintXXXX constants below
|
||||||
|
// for a list of well-known display hint annotations.
|
||||||
|
DisplayHintAnnotation = "safing/portbase:ui:display-hint"
|
||||||
|
// DisplayOrderAnnotation provides a hint for the user
|
||||||
|
// interface in which order settings should be displayed.
|
||||||
|
// The value of DisplayOrderAnnotations is expected to be
|
||||||
|
// an number (int).
|
||||||
|
DisplayOrderAnnotation = "safing/portbase:ui:order"
|
||||||
|
// UnitAnnotations defines the SI unit of an option (if any).
|
||||||
|
UnitAnnotation = "safing/portbase:ui:unit"
|
||||||
|
// CategoryAnnotations can provide an additional category
|
||||||
|
// to each settings. This category can be used by a user
|
||||||
|
// interface to group certain options together.
|
||||||
|
// User interfaces should treat a CategoryAnnotation, if
|
||||||
|
// supported, with higher priority as a DisplayOrderAnnotation.
|
||||||
|
CategoryAnnotation = "safing/portbase:ui:category"
|
||||||
|
// SubsystemAnnotation can be used to mark an option as part
|
||||||
|
// of a module subsystem.
|
||||||
|
SubsystemAnnotation = "safing/portbase:module:subsystem"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Values for the DisplayHintAnnotation
|
||||||
|
const (
|
||||||
|
// DisplayHintOneOf is used to mark an option
|
||||||
|
// as a "select"-style option. That is, only one of
|
||||||
|
// the supported values may be set. This option makes
|
||||||
|
// only sense together with the PossibleValues property
|
||||||
|
// of Option.
|
||||||
|
DisplayHintOneOf = "one-of"
|
||||||
|
// DisplayHintOrdered Used to mark a list option as ordered.
|
||||||
|
// That is, the order of items is important and a user interface
|
||||||
|
// is encouraged to provide the user with re-ordering support
|
||||||
|
// (like drag'n'drop).
|
||||||
|
DisplayHintOrdered = "ordered"
|
||||||
|
)
|
||||||
|
|
||||||
// Option describes a configuration option.
|
// Option describes a configuration option.
|
||||||
type Option struct {
|
type Option struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
|
// Name holds the name of the configuration options.
|
||||||
|
// It should be human readable and is mainly used for
|
||||||
|
// presentation purposes.
|
||||||
|
// Name is considered immutable after the option has
|
||||||
|
// been created.
|
||||||
Name string
|
Name string
|
||||||
Key string // in path format: category/sub/key
|
// Key holds the database path for the option. It should
|
||||||
|
// follow the path format `category/sub/key`.
|
||||||
|
// Key is considered immutable after the option has
|
||||||
|
// been created.
|
||||||
|
Key string
|
||||||
|
// Description holds a human readable description of the
|
||||||
|
// option and what is does. The description should be short.
|
||||||
|
// Use the Help property for a longer support text.
|
||||||
|
// Description is considered immutable after the option has
|
||||||
|
// been created.
|
||||||
Description string
|
Description string
|
||||||
|
// Help may hold a long version of the description providing
|
||||||
|
// assistence with the configuration option.
|
||||||
|
// Help is considered immutable after the option has
|
||||||
|
// been created.
|
||||||
Help string
|
Help string
|
||||||
Order int
|
// OptType defines the type of the option.
|
||||||
|
// OptType is considered immutable after the option has
|
||||||
OptType uint8
|
// been created.
|
||||||
ExpertiseLevel uint8
|
OptType OptionType
|
||||||
ReleaseLevel uint8
|
// ExpertiseLevel can be used to set the required expertise
|
||||||
|
// level for the option to be displayed to a user.
|
||||||
|
// ExpertiseLevel is considered immutable after the option has
|
||||||
|
// been created.
|
||||||
|
ExpertiseLevel ExpertiseLevel
|
||||||
|
// ReleaseLevel is used to mark the stability of the option.
|
||||||
|
// ReleaseLevel is considered immutable after the option has
|
||||||
|
// been created.
|
||||||
|
ReleaseLevel ReleaseLevel
|
||||||
|
// RequiresRestart should be set to true if a modification of
|
||||||
|
// the options value requires a restart of the whole application
|
||||||
|
// to take effect.
|
||||||
|
// RequiresRestart is considered immutable after the option has
|
||||||
|
// been created.
|
||||||
RequiresRestart bool
|
RequiresRestart bool
|
||||||
|
// DefaultValue holds the default value of the option. Note that
|
||||||
|
// this value can be overwritten during runtime (see activeDefaultValue
|
||||||
|
// and activeFallbackValue).
|
||||||
|
// DefaultValue is considered immutable after the option has
|
||||||
|
// been created.
|
||||||
DefaultValue interface{}
|
DefaultValue interface{}
|
||||||
|
// ValidationRegex may contain a regular expression used to validate
|
||||||
ExternalOptType string
|
// the value of option. If the option type is set to OptTypeStringArray
|
||||||
|
// the validation regex is applied to all entries of the string slice.
|
||||||
|
// Note that it is recommended to keep the validation regex simple so
|
||||||
|
// it can also be used in other languages (mainly JavaScript) to provide
|
||||||
|
// a better user-experience by pre-validating the expression.
|
||||||
|
// ValidationRegex is considered immutable after the option has
|
||||||
|
// been created.
|
||||||
ValidationRegex string
|
ValidationRegex string
|
||||||
|
// PossibleValues may be set to a slice of values that are allowed
|
||||||
|
// for this configuration setting. Note that PossibleValues makes most
|
||||||
|
// sense when ExternalOptType is set to HintOneOf
|
||||||
|
// PossibleValues is considered immutable after the option has
|
||||||
|
// been created.
|
||||||
|
PossibleValues []PossibleValue `json:",omitempty"`
|
||||||
|
// Annotations adds additional annotations to the configuration options.
|
||||||
|
// See documentation of Annotations for more information.
|
||||||
|
// Annotations is considered mutable and setting/reading annotation keys
|
||||||
|
// must be performed while the option is locked.
|
||||||
|
Annotations Annotations
|
||||||
|
|
||||||
activeValue *valueCache // runtime value (loaded from config file or set by user)
|
activeValue *valueCache // runtime value (loaded from config file or set by user)
|
||||||
activeDefaultValue *valueCache // runtime default value (may be set internally)
|
activeDefaultValue *valueCache // runtime default value (may be set internally)
|
||||||
|
@ -60,6 +178,45 @@ type Option struct {
|
||||||
compiledRegex *regexp.Regexp
|
compiledRegex *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddAnnotation adds the annotation key to option if it's not already set.
|
||||||
|
func (option *Option) AddAnnotation(key string, value interface{}) {
|
||||||
|
option.Lock()
|
||||||
|
defer option.Unlock()
|
||||||
|
|
||||||
|
if option.Annotations == nil {
|
||||||
|
option.Annotations = make(Annotations)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := option.Annotations[key]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
option.Annotations[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAnnotation sets the value of the annotation key overwritting an
|
||||||
|
// existing value if required.
|
||||||
|
func (option *Option) SetAnnotation(key string, value interface{}) {
|
||||||
|
option.Lock()
|
||||||
|
defer option.Unlock()
|
||||||
|
|
||||||
|
if option.Annotations == nil {
|
||||||
|
option.Annotations = make(Annotations)
|
||||||
|
}
|
||||||
|
option.Annotations[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAnnotation returns the value of the annotation key
|
||||||
|
func (option *Option) GetAnnotation(key string) (interface{}, bool) {
|
||||||
|
option.Lock()
|
||||||
|
defer option.Unlock()
|
||||||
|
|
||||||
|
if option.Annotations == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
val, ok := option.Annotations[key]
|
||||||
|
return val, ok
|
||||||
|
}
|
||||||
|
|
||||||
// Export expors an option to a Record.
|
// Export expors an option to a Record.
|
||||||
func (option *Option) Export() (record.Record, error) {
|
func (option *Option) Export() (record.Record, error) {
|
||||||
option.Lock()
|
option.Lock()
|
||||||
|
@ -93,20 +250,8 @@ func (option *Option) Export() (record.Record, error) {
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type sortableOptions []*Option
|
type sortByKey []*Option
|
||||||
|
|
||||||
// Len is the number of elements in the collection.
|
func (opts sortByKey) Len() int { return len(opts) }
|
||||||
func (opts sortableOptions) Len() int {
|
func (opts sortByKey) Less(i, j int) bool { return opts[i].Key < opts[j].Key }
|
||||||
return len(opts)
|
func (opts sortByKey) Swap(i, j int) { opts[i], opts[j] = opts[j], opts[i] }
|
||||||
}
|
|
||||||
|
|
||||||
// Less reports whether the element with
|
|
||||||
// index i should sort before the element with index j.
|
|
||||||
func (opts sortableOptions) Less(i, j int) bool {
|
|
||||||
return opts[i].Key < opts[j].Key
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swap swaps the elements with indexes i and j.
|
|
||||||
func (opts sortableOptions) Swap(i, j int) {
|
|
||||||
opts[i], opts[j] = opts[j], opts[i]
|
|
||||||
}
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ optionsLoop:
|
||||||
return perspective, nil
|
return perspective, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Perspective) getPerspectiveValueCache(name string, requestedType uint8) *valueCache {
|
func (p *Perspective) getPerspectiveValueCache(name string, requestedType OptionType) *valueCache {
|
||||||
// get option
|
// get option
|
||||||
pOption, ok := p.config[name]
|
pOption, ok := p.config[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package config
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -11,6 +12,23 @@ var (
|
||||||
options = make(map[string]*Option)
|
options = make(map[string]*Option)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ForEachOption calls fn for each defined option. If fn returns
|
||||||
|
// and error the iteration is stopped and the error is returned.
|
||||||
|
// Note that ForEachOption does not guarantee a stable order of
|
||||||
|
// iteration between multiple calles. ForEachOption does NOT lock
|
||||||
|
// opt when calling fn.
|
||||||
|
func ForEachOption(fn func(opt *Option) error) error {
|
||||||
|
optionsLock.Lock()
|
||||||
|
defer optionsLock.Unlock()
|
||||||
|
|
||||||
|
for _, opt := range options {
|
||||||
|
if err := fn(opt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Register registers a new configuration option.
|
// Register registers a new configuration option.
|
||||||
func Register(option *Option) error {
|
func Register(option *Option) error {
|
||||||
if option.Name == "" {
|
if option.Name == "" {
|
||||||
|
@ -26,8 +44,15 @@ func Register(option *Option) error {
|
||||||
return fmt.Errorf("failed to register option: please set option.OptType")
|
return fmt.Errorf("failed to register option: please set option.OptType")
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
if option.ValidationRegex == "" && option.PossibleValues != nil {
|
||||||
|
values := make([]string, len(option.PossibleValues))
|
||||||
|
for idx, val := range option.PossibleValues {
|
||||||
|
values[idx] = fmt.Sprintf("%v", val.Value)
|
||||||
|
}
|
||||||
|
option.ValidationRegex = fmt.Sprintf("^(%s)$", strings.Join(values, "|"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
if option.ValidationRegex != "" {
|
if option.ValidationRegex != "" {
|
||||||
option.compiledRegex, err = regexp.Compile(option.ValidationRegex)
|
option.compiledRegex, err = regexp.Compile(option.ValidationRegex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -3,17 +3,20 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/tevino/abool"
|
"github.com/tevino/abool"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ReleaseLevel is used to define the maturity of a
|
||||||
|
// configuration setting.
|
||||||
|
type ReleaseLevel uint8
|
||||||
|
|
||||||
// Release Level constants
|
// Release Level constants
|
||||||
const (
|
const (
|
||||||
ReleaseLevelStable uint8 = 0
|
ReleaseLevelStable ReleaseLevel = 0
|
||||||
ReleaseLevelBeta uint8 = 1
|
ReleaseLevelBeta ReleaseLevel = 1
|
||||||
ReleaseLevelExperimental uint8 = 2
|
ReleaseLevelExperimental ReleaseLevel = 2
|
||||||
|
|
||||||
ReleaseLevelNameStable = "stable"
|
ReleaseLevelNameStable = "stable"
|
||||||
ReleaseLevelNameBeta = "beta"
|
ReleaseLevelNameBeta = "beta"
|
||||||
|
@ -23,15 +26,12 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
releaseLevel *int32
|
releaseLevel = new(int32)
|
||||||
releaseLevelOption *Option
|
releaseLevelOption *Option
|
||||||
releaseLevelOptionFlag = abool.New()
|
releaseLevelOptionFlag = abool.New()
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
var releaseLevelVal int32
|
|
||||||
releaseLevel = &releaseLevelVal
|
|
||||||
|
|
||||||
registerReleaseLevelOption()
|
registerReleaseLevelOption()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,16 +40,30 @@ func registerReleaseLevelOption() {
|
||||||
Name: "Release Level",
|
Name: "Release Level",
|
||||||
Key: releaseLevelKey,
|
Key: releaseLevelKey,
|
||||||
Description: "The Release Level changes which features are available to you. Some beta or experimental features are also available in the stable release channel. Unavailable settings are set to the default value.",
|
Description: "The Release Level changes which features are available to you. Some beta or experimental features are also available in the stable release channel. Unavailable settings are set to the default value.",
|
||||||
|
|
||||||
OptType: OptTypeString,
|
OptType: OptTypeString,
|
||||||
ExpertiseLevel: ExpertiseLevelExpert,
|
ExpertiseLevel: ExpertiseLevelExpert,
|
||||||
ReleaseLevel: ReleaseLevelStable,
|
ReleaseLevel: ReleaseLevelStable,
|
||||||
|
|
||||||
RequiresRestart: false,
|
|
||||||
DefaultValue: ReleaseLevelNameStable,
|
DefaultValue: ReleaseLevelNameStable,
|
||||||
|
Annotations: Annotations{
|
||||||
ExternalOptType: "string list",
|
DisplayHintAnnotation: DisplayHintOneOf,
|
||||||
ValidationRegex: fmt.Sprintf("^(%s|%s|%s)$", ReleaseLevelNameStable, ReleaseLevelNameBeta, ReleaseLevelNameExperimental),
|
},
|
||||||
|
PossibleValues: []PossibleValue{
|
||||||
|
{
|
||||||
|
Name: "Stable",
|
||||||
|
Value: ReleaseLevelNameStable,
|
||||||
|
Description: "Only show stable features.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Beta",
|
||||||
|
Value: ReleaseLevelNameBeta,
|
||||||
|
Description: "Show stable and beta features.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Experimental",
|
||||||
|
Value: ReleaseLevelNameExperimental,
|
||||||
|
Description: "Show experimental features",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := Register(releaseLevelOption)
|
err := Register(releaseLevelOption)
|
||||||
|
@ -86,6 +100,6 @@ func updateReleaseLevel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getReleaseLevel() uint8 {
|
func getReleaseLevel() ReleaseLevel {
|
||||||
return uint8(atomic.LoadInt32(releaseLevel))
|
return ReleaseLevel(atomic.LoadInt32(releaseLevel))
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
type valueCache struct {
|
type valueCache struct {
|
||||||
|
@ -28,7 +29,48 @@ func (vc *valueCache) getData(opt *Option) interface{} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isAllowedPossibleValue checks if value is defined as a PossibleValue
|
||||||
|
// in opt. If there are not possible values defined value is considered
|
||||||
|
// allowed and nil is returned. isAllowedPossibleValue ensure the actual
|
||||||
|
// value is an allowed primitiv value by using reflection to convert
|
||||||
|
// value and each PossibleValue to a comparable primitiv if possible.
|
||||||
|
// In case of complex value types isAllowedPossibleValue uses
|
||||||
|
// reflect.DeepEqual as a fallback.
|
||||||
|
func isAllowedPossibleValue(opt *Option, value interface{}) error {
|
||||||
|
if opt.PossibleValues == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, val := range opt.PossibleValues {
|
||||||
|
compareAgainst := val.Value
|
||||||
|
valueType := reflect.TypeOf(value)
|
||||||
|
|
||||||
|
// loading int's from the configuration JSON does not perserve the correct type
|
||||||
|
// as we get float64 instead. Make sure to convert them before.
|
||||||
|
if reflect.TypeOf(val.Value).ConvertibleTo(valueType) {
|
||||||
|
compareAgainst = reflect.ValueOf(val.Value).Convert(valueType).Interface()
|
||||||
|
}
|
||||||
|
if compareAgainst == value {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if reflect.DeepEqual(val.Value, value) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("value is not allowed")
|
||||||
|
}
|
||||||
|
|
||||||
func validateValue(option *Option, value interface{}) (*valueCache, error) { //nolint:gocyclo
|
func validateValue(option *Option, value interface{}) (*valueCache, error) { //nolint:gocyclo
|
||||||
|
if option.OptType != OptTypeStringArray {
|
||||||
|
if err := isAllowedPossibleValue(option, value); err != nil {
|
||||||
|
return nil, fmt.Errorf("validation of option %s failed for %v: %w", option.Key, value, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reflect.TypeOf(value).ConvertibleTo(reflect.TypeOf(""))
|
||||||
|
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
case string:
|
case string:
|
||||||
if option.OptType != OptTypeString {
|
if option.OptType != OptTypeString {
|
||||||
|
@ -61,6 +103,10 @@ func validateValue(option *Option, value interface{}) (*valueCache, error) { //n
|
||||||
if !option.compiledRegex.MatchString(entry) {
|
if !option.compiledRegex.MatchString(entry) {
|
||||||
return nil, fmt.Errorf("validation of option %s failed: string \"%s\" at index %d did not match validation regex", option.Key, entry, pos)
|
return nil, fmt.Errorf("validation of option %s failed: string \"%s\" at index %d did not match validation regex", option.Key, entry, pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := isAllowedPossibleValue(option, entry); err != nil {
|
||||||
|
return nil, fmt.Errorf("validation of option %s failed: string %q at index %d is not allowed", option.Key, entry, pos)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &valueCache{stringArrayVal: v}, nil
|
return &valueCache{stringArrayVal: v}, nil
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/config"
|
||||||
"github.com/safing/portbase/database"
|
"github.com/safing/portbase/database"
|
||||||
_ "github.com/safing/portbase/database/dbmodule" // database module is required
|
_ "github.com/safing/portbase/database/dbmodule" // database module is required
|
||||||
"github.com/safing/portbase/modules"
|
"github.com/safing/portbase/modules"
|
||||||
|
@ -25,10 +26,14 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// enable partial starting
|
// The subsystem layer takes over module management. Note that
|
||||||
|
// no one must have called EnableModuleManagement. Otherwise
|
||||||
|
// the subsystem layer will silently fail managing module
|
||||||
|
// dependencies!
|
||||||
|
// TODO(ppacher): we SHOULD panic here!
|
||||||
|
// TASK(#1431)
|
||||||
modules.EnableModuleManagement(handleModuleChanges)
|
modules.EnableModuleManagement(handleModuleChanges)
|
||||||
|
|
||||||
// register module and enable it for starting
|
|
||||||
module = modules.Register("subsystems", prep, start, nil, "config", "database", "base")
|
module = modules.Register("subsystems", prep, start, nil, "config", "database", "base")
|
||||||
module.Enable()
|
module.Enable()
|
||||||
|
|
||||||
|
@ -44,29 +49,59 @@ func prep() error {
|
||||||
return modules.ErrCleanExit
|
return modules.ErrCleanExit
|
||||||
}
|
}
|
||||||
|
|
||||||
return module.RegisterEventHook("config", configChangeEvent, "control subsystems", handleConfigChanges)
|
// We need to listen for configuration changes so we can
|
||||||
|
// start/stop dependend modules in case a subsystem is
|
||||||
|
// (de-)activated.
|
||||||
|
if err := module.RegisterEventHook(
|
||||||
|
"config",
|
||||||
|
configChangeEvent,
|
||||||
|
"control subsystems",
|
||||||
|
handleConfigChanges,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("register event hook: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() error {
|
func start() error {
|
||||||
// lock registration
|
// Registration of subsystems is only allowed during
|
||||||
|
// preperation. Make sure any further call to Register()
|
||||||
|
// panics.
|
||||||
subsystemsLocked.Set()
|
subsystemsLocked.Set()
|
||||||
|
|
||||||
// lock slice and map
|
|
||||||
subsystemsLock.Lock()
|
subsystemsLock.Lock()
|
||||||
// go through all dependencies
|
defer subsystemsLock.Unlock()
|
||||||
seen := make(map[string]struct{})
|
|
||||||
|
seen := make(map[string]struct{}, len(subsystems))
|
||||||
|
configKeyPrefixes := make(map[string]*Subsystem, len(subsystems))
|
||||||
|
// mark all sub-systems as seen. This prevents sub-systems
|
||||||
|
// from being added as a sub-systems dependency in addAndMarkDependencies.
|
||||||
for _, sub := range subsystems {
|
for _, sub := range subsystems {
|
||||||
// mark subsystem module as seen
|
|
||||||
seen[sub.module.Name] = struct{}{}
|
seen[sub.module.Name] = struct{}{}
|
||||||
|
configKeyPrefixes[sub.ConfigKeySpace] = sub
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// aggregate all modules dependencies (and the subsystem module itself)
|
||||||
|
// into the Modules slice. Configuration options form dependened modules
|
||||||
|
// will be marked using config.SubsystemAnnotation if not already set.
|
||||||
for _, sub := range subsystems {
|
for _, sub := range subsystems {
|
||||||
// add main module
|
|
||||||
sub.Modules = append(sub.Modules, statusFromModule(sub.module))
|
sub.Modules = append(sub.Modules, statusFromModule(sub.module))
|
||||||
// add dependencies
|
|
||||||
sub.addDependencies(sub.module, seen)
|
sub.addDependencies(sub.module, seen)
|
||||||
}
|
}
|
||||||
// unlock
|
|
||||||
subsystemsLock.Unlock()
|
// Annotate all configuration options with their respective subsystem.
|
||||||
|
config.ForEachOption(func(opt *config.Option) error {
|
||||||
|
subsys, ok := configKeyPrefixes[opt.Key]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new subsystem annotation is it is not already set!
|
||||||
|
opt.AddAnnotation(config.SubsystemAnnotation, subsys.ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
// apply config
|
// apply config
|
||||||
module.StartWorker("initial subsystem configuration", func(ctx context.Context) error {
|
module.StartWorker("initial subsystem configuration", func(ctx context.Context) error {
|
||||||
|
@ -77,13 +112,10 @@ func start() error {
|
||||||
|
|
||||||
func (sub *Subsystem) addDependencies(module *modules.Module, seen map[string]struct{}) {
|
func (sub *Subsystem) addDependencies(module *modules.Module, seen map[string]struct{}) {
|
||||||
for _, module := range module.Dependencies() {
|
for _, module := range module.Dependencies() {
|
||||||
_, ok := seen[module.Name]
|
if _, ok := seen[module.Name]; !ok {
|
||||||
if !ok {
|
|
||||||
// add dependency to modules
|
|
||||||
sub.Modules = append(sub.Modules, statusFromModule(module))
|
|
||||||
// mark as seen
|
|
||||||
seen[module.Name] = struct{}{}
|
seen[module.Name] = struct{}{}
|
||||||
// add further dependencies
|
|
||||||
|
sub.Modules = append(sub.Modules, statusFromModule(module))
|
||||||
sub.addDependencies(module, seen)
|
sub.addDependencies(module, seen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,7 +141,7 @@ func printGraph() {
|
||||||
for _, sub := range subsystems {
|
for _, sub := range subsystems {
|
||||||
sub.module.Enable() // mark as tree root
|
sub.module.Enable() // mark as tree root
|
||||||
}
|
}
|
||||||
// print
|
|
||||||
for _, sub := range subsystems {
|
for _, sub := range subsystems {
|
||||||
printModuleGraph("", sub.module, true)
|
printModuleGraph("", sub.module, true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,26 +9,46 @@ import (
|
||||||
"github.com/safing/portbase/modules"
|
"github.com/safing/portbase/modules"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Subsystem describes a subset of modules that represent a part of a service or program to the user.
|
// Subsystem describes a subset of modules that represent a part of a
|
||||||
|
// service or program to the user. Subsystems can be (de-)activated causing
|
||||||
|
// all related modules to be brought down or up.
|
||||||
type Subsystem struct { //nolint:maligned // not worth the effort
|
type Subsystem struct { //nolint:maligned // not worth the effort
|
||||||
record.Base
|
record.Base
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
|
// ID is a unique identifier for the subsystem.
|
||||||
ID string
|
ID string
|
||||||
|
// Name holds a human readable name of the subsystem.
|
||||||
Name string
|
Name string
|
||||||
|
// Description may holds an optional description of
|
||||||
|
// the subsystem's purpose.
|
||||||
Description string
|
Description string
|
||||||
module *modules.Module
|
// Modules contains all modules that are related to the subsystem.
|
||||||
|
// Note that this slice also contains a reference to the subsystem
|
||||||
|
// module itself.
|
||||||
Modules []*ModuleStatus
|
Modules []*ModuleStatus
|
||||||
FailureStatus uint8 // summary: worst status
|
// FailureStatus is the worst failure status that is currently
|
||||||
|
// set in one of the subsystem's dependencies.
|
||||||
|
FailureStatus uint8
|
||||||
|
// ToggleOptionKey holds the key of the configuraiton option
|
||||||
|
// that is used to completely enable or disable this subsystem.
|
||||||
ToggleOptionKey string
|
ToggleOptionKey string
|
||||||
toggleOption *config.Option
|
// ExpertiseLevel defines the complexity of the subsystem and is
|
||||||
toggleValue func() bool
|
// copied from the subsystem's toggleOption.
|
||||||
ExpertiseLevel uint8 // copied from toggleOption
|
ExpertiseLevel config.ExpertiseLevel
|
||||||
ReleaseLevel uint8 // copied from toggleOption
|
// ReleaseLevel defines the stability of the subsystem and is
|
||||||
|
// copied form the subsystem's toggleOption.
|
||||||
|
ReleaseLevel config.ReleaseLevel
|
||||||
|
// ConfigKeySpace defines the database key prefix that all
|
||||||
|
// options that belong to this subsystem have. Note that this
|
||||||
|
// value is mainly used to mark all related options with a
|
||||||
|
// config.SubsystemAnnotation. Options that are part of
|
||||||
|
// this subsystem but don't start with the correct prefix can
|
||||||
|
// still be marked by manually setting the appropriate annotation.
|
||||||
ConfigKeySpace string
|
ConfigKeySpace string
|
||||||
|
|
||||||
|
module *modules.Module
|
||||||
|
toggleOption *config.Option
|
||||||
|
toggleValue config.BoolOption
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModuleStatus describes the status of a module.
|
// ModuleStatus describes the status of a module.
|
||||||
|
@ -48,16 +68,19 @@ type ModuleStatus struct {
|
||||||
|
|
||||||
// Save saves the Subsystem Status to the database.
|
// Save saves the Subsystem Status to the database.
|
||||||
func (sub *Subsystem) Save() {
|
func (sub *Subsystem) Save() {
|
||||||
if databaseKeySpace != "" {
|
if databaseKeySpace == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !sub.KeyIsSet() {
|
if !sub.KeyIsSet() {
|
||||||
sub.SetKey(databaseKeySpace + sub.ID)
|
sub.SetKey(databaseKeySpace + sub.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := db.Put(sub)
|
err := db.Put(sub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("subsystems: could not save subsystem status to database: %s", err)
|
log.Errorf("subsystems: could not save subsystem status to database: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func statusFromModule(module *modules.Module) *ModuleStatus {
|
func statusFromModule(module *modules.Module) *ModuleStatus {
|
||||||
status := &ModuleStatus{
|
status := &ModuleStatus{
|
||||||
|
@ -90,6 +113,7 @@ func compareAndUpdateStatus(module *modules.Module, status *ModuleStatus) (chang
|
||||||
failureStatus, failureID, failureMsg := module.FailureStatus()
|
failureStatus, failureID, failureMsg := module.FailureStatus()
|
||||||
if status.FailureStatus != failureStatus ||
|
if status.FailureStatus != failureStatus ||
|
||||||
status.FailureID != failureID {
|
status.FailureID != failureID {
|
||||||
|
|
||||||
status.FailureStatus = failureStatus
|
status.FailureStatus = failureStatus
|
||||||
status.FailureID = failureID
|
status.FailureID = failureID
|
||||||
status.FailureMsg = failureMsg
|
status.FailureMsg = failureMsg
|
||||||
|
|
|
@ -13,15 +13,18 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
subsystemsLock sync.Mutex
|
||||||
subsystems []*Subsystem
|
subsystems []*Subsystem
|
||||||
subsystemsMap = make(map[string]*Subsystem)
|
subsystemsMap = make(map[string]*Subsystem)
|
||||||
subsystemsLock sync.Mutex
|
|
||||||
subsystemsLocked = abool.New()
|
subsystemsLocked = abool.New()
|
||||||
|
|
||||||
handlingConfigChanges = abool.New()
|
handlingConfigChanges = abool.New()
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register registers a new subsystem. The given option must be a bool option. Should be called in init() directly after the modules.Register() function. The config option must not yet be registered and will be registered for you. Pass a nil option to force enable.
|
// Register registers a new subsystem. The given option must be a bool option.
|
||||||
|
// Should be called in init() directly after the modules.Register() function.
|
||||||
|
// The config option must not yet be registered and will be registered for
|
||||||
|
// you. Pass a nil option to force enable.
|
||||||
func Register(id, name, description string, module *modules.Module, configKeySpace string, option *config.Option) {
|
func Register(id, name, description string, module *modules.Module, configKeySpace string, option *config.Option) {
|
||||||
// lock slice and map
|
// lock slice and map
|
||||||
subsystemsLock.Lock()
|
subsystemsLock.Lock()
|
||||||
|
@ -33,8 +36,7 @@ func Register(id, name, description string, module *modules.Module, configKeySpa
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if already registered
|
// check if already registered
|
||||||
_, ok := subsystemsMap[name]
|
if _, ok := subsystemsMap[name]; ok {
|
||||||
if ok {
|
|
||||||
panic(fmt.Sprintf(`subsystem "%s" already registered`, name))
|
panic(fmt.Sprintf(`subsystem "%s" already registered`, name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +67,6 @@ func Register(id, name, description string, module *modules.Module, configKeySpa
|
||||||
new.toggleValue = func() bool { return true }
|
new.toggleValue = func() bool { return true }
|
||||||
}
|
}
|
||||||
|
|
||||||
// add to lists
|
|
||||||
subsystemsMap[name] = new
|
subsystemsMap[name] = new
|
||||||
subsystems = append(subsystems, new)
|
subsystems = append(subsystems, new)
|
||||||
}
|
}
|
||||||
|
@ -113,9 +114,10 @@ subsystemLoop:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleConfigChanges(ctx context.Context, data interface{}) error {
|
func handleConfigChanges(ctx context.Context, _ interface{}) error {
|
||||||
// check if ready
|
// bail out early if we haven't started yet or are already
|
||||||
if !subsystemsLocked.IsSet() {
|
// shutting down
|
||||||
|
if !subsystemsLocked.IsSet() || modules.IsShuttingDown() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,11 +129,6 @@ func handleConfigChanges(ctx context.Context, data interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't do anything if we are already shutting down globally
|
|
||||||
if modules.IsShuttingDown() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// only run one instance at any time
|
// only run one instance at any time
|
||||||
subsystemsLock.Lock()
|
subsystemsLock.Lock()
|
||||||
defer subsystemsLock.Unlock()
|
defer subsystemsLock.Unlock()
|
||||||
|
@ -143,9 +140,10 @@ func handleConfigChanges(ctx context.Context, data interface{}) error {
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !changed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// trigger module management if any setting was changed
|
|
||||||
if changed {
|
|
||||||
err := modules.ManageModules()
|
err := modules.ManageModules()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
module.Error(
|
module.Error(
|
||||||
|
@ -155,7 +153,6 @@ func handleConfigChanges(ctx context.Context, data interface{}) error {
|
||||||
} else {
|
} else {
|
||||||
module.Resolve("modulemgmt-failed")
|
module.Resolve("modulemgmt-failed")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ func prep() error {
|
||||||
// register options
|
// register options
|
||||||
err := config.Register(&config.Option{
|
err := config.Register(&config.Option{
|
||||||
Name: "language",
|
Name: "language",
|
||||||
Key: "config:template/language",
|
Key: "template/language",
|
||||||
Description: "Sets the language for the template [TEMPLATE]",
|
Description: "Sets the language for the template [TEMPLATE]",
|
||||||
OptType: config.OptTypeString,
|
OptType: config.OptTypeString,
|
||||||
ExpertiseLevel: config.ExpertiseLevelUser, // default
|
ExpertiseLevel: config.ExpertiseLevelUser, // default
|
||||||
|
|
Loading…
Add table
Reference in a new issue