[#493] cmd/node: Implement a basic configuration component

Create `config` package nearby storage node application. Implement `Config`
as a wrapper over `viper.Viper` that provides the minimum functionality
required by the application.

The constructor allows you to read the config from the file. Methods are
provided for reading subsections and values from the config tree. Helper
functions are implemented to cast a value to native Go types.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
Leonard Lyubich 2021-05-21 14:50:40 +03:00 committed by Leonard Lyubich
parent d34de558f0
commit 7e11bf9a55
11 changed files with 260 additions and 0 deletions

View file

@ -0,0 +1,28 @@
package config
import (
"github.com/spf13/viper"
)
// Sub returns sub-section of the Config by name.
func (x *Config) Sub(name string) *Config {
return (*Config)(
(*viper.Viper)(x).Sub(name),
)
}
// Value returns configuration value by name.
//
// Result can be casted to a particular type
// via corresponding function (e.g. StringSlice).
// Note: casting via Go `.()` operator is not
// recommended.
//
// Returns nil if config is nil.
func (x *Config) Value(name string) interface{} {
if x != nil {
return (*viper.Viper)(x).Get(name)
}
return nil
}

View file

@ -0,0 +1,29 @@
package config_test
import (
"testing"
"github.com/nspcc-dev/neofs-node/cmd/neofs-node/config"
"github.com/stretchr/testify/require"
)
func TestConfigCommon(t *testing.T) {
forEachFileType("test/config", func(c *config.Config) {
val := c.Value("value")
require.NotNil(t, val)
val = c.Value("non-existent value")
require.Nil(t, val)
sub := c.Sub("section")
require.NotNil(t, sub)
const nonExistentSub = "non-existent sub-section"
sub = c.Sub(nonExistentSub)
require.Nil(t, sub)
val = c.Sub(nonExistentSub).Value("value")
require.Nil(t, val)
})
}

View file

@ -0,0 +1,30 @@
package config
import (
"github.com/spf13/cast"
)
func panicOnErr(err error) {
if err != nil {
panic(err)
}
}
// StringSlice reads configuration value
// from c by name and casts it to []string.
//
// Panics if value can not be casted.
func StringSlice(c *Config, name string) []string {
x, err := cast.ToStringSliceE(c.Value(name))
panicOnErr(err)
return x
}
// StringSliceSafe reads configuration value
// from c by name and casts it to []string.
//
// Returns nil if value can not be casted.
func StringSliceSafe(c *Config, name string) []string {
return cast.ToStringSlice(c.Value(name))
}

View file

@ -0,0 +1,30 @@
package config_test
import (
"testing"
"github.com/nspcc-dev/neofs-node/cmd/neofs-node/config"
"github.com/stretchr/testify/require"
)
func TestStringSlice(t *testing.T) {
forEachFileType("test/config", func(c *config.Config) {
cStringSlice := c.Sub("string_slice")
val := config.StringSlice(cStringSlice, "empty")
require.Empty(t, val)
val = config.StringSlice(cStringSlice, "filled")
require.Equal(t, []string{
"string1",
"string2",
}, val)
require.Panics(t, func() {
config.StringSlice(cStringSlice, "incorrect")
})
val = config.StringSliceSafe(cStringSlice, "incorrect")
require.Nil(t, val)
})
}

View file

@ -0,0 +1,53 @@
package config
import (
"fmt"
"strings"
"github.com/spf13/viper"
)
// Config represents a group of named values structured
// by tree type.
//
// Sub-trees are named configuration sub-sections,
// leaves are named configuration values.
// Names are of string type.
type Config viper.Viper
const (
separator = "."
envPrefix = "neofs"
envSeparator = "_"
)
// Prm groups required parameters of the Config.
type Prm struct{}
// New creates a new Config instance.
//
// If file option is provided (WithConfigFile),
// configuration values are read from it.
// Otherwise, Config is a degenerate tree.
func New(_ Prm, opts ...Option) *Config {
v := viper.New()
o := defaultOpts()
for i := range opts {
opts[i](o)
}
if o.path != "" {
v.SetEnvPrefix(envPrefix)
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(separator, envSeparator))
v.SetConfigFile(o.path)
err := v.ReadInConfig()
if err != nil {
panic(fmt.Errorf("failed to read config: %v", err))
}
}
return (*Config)(v)
}

View file

@ -0,0 +1,26 @@
package config_test
import (
"github.com/nspcc-dev/neofs-node/cmd/neofs-node/config"
)
func fromFile(path string) *config.Config {
var p config.Prm
return config.New(p,
config.WithConfigFile(path),
)
}
func forEachFile(paths []string, f func(*config.Config)) {
for i := range paths {
f(fromFile(paths[i]))
}
}
func forEachFileType(pref string, f func(*config.Config)) {
forEachFile([]string{
pref + ".yaml",
pref + ".json",
}, f)
}

View file

@ -0,0 +1,20 @@
package config
type opts struct {
path string
}
func defaultOpts() *opts {
return new(opts)
}
// Option allows to set optional parameter of the Config.
type Option func(*opts)
// WithConfigFile returns option to set system path
// to the configuration file.
func WithConfigFile(path string) Option {
return func(o *opts) {
o.path = path
}
}

View file

@ -0,0 +1,14 @@
{
"value": "some value",
"section": {
"any": "thing"
},
"string_slice": {
"empty": [],
"filled": [
"string1",
"string2"
],
"incorrect": null
}
}

View file

@ -0,0 +1,13 @@
value: some value
section:
any: thing
string_slice:
empty: []
filled:
- string1
- string2
incorrect:

View file

@ -0,0 +1,16 @@
package config
import (
"github.com/nspcc-dev/neofs-node/misc"
)
// DebugValue returns debug configuration value.
//
// Returns nil if misc.Debug is not set to "true".
func DebugValue(c *Config, name string) interface{} {
if misc.Debug == "true" {
return c.Value(name)
}
return nil
}

1
go.mod
View file

@ -20,6 +20,7 @@ require (
github.com/panjf2000/ants/v2 v2.3.0
github.com/paulmach/orb v0.2.1
github.com/prometheus/client_golang v1.6.0
github.com/spf13/cast v1.3.0
github.com/spf13/cobra v1.0.0
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.7.0