diff --git a/cmd/neofs-node/config/calls.go b/cmd/neofs-node/config/calls.go new file mode 100644 index 00000000..1541557e --- /dev/null +++ b/cmd/neofs-node/config/calls.go @@ -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 +} diff --git a/cmd/neofs-node/config/calls_test.go b/cmd/neofs-node/config/calls_test.go new file mode 100644 index 00000000..b66eea7c --- /dev/null +++ b/cmd/neofs-node/config/calls_test.go @@ -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) + }) +} diff --git a/cmd/neofs-node/config/cast.go b/cmd/neofs-node/config/cast.go new file mode 100644 index 00000000..dbd39b55 --- /dev/null +++ b/cmd/neofs-node/config/cast.go @@ -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)) +} diff --git a/cmd/neofs-node/config/cast_test.go b/cmd/neofs-node/config/cast_test.go new file mode 100644 index 00000000..9eefd50c --- /dev/null +++ b/cmd/neofs-node/config/cast_test.go @@ -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) + }) +} diff --git a/cmd/neofs-node/config/config.go b/cmd/neofs-node/config/config.go new file mode 100644 index 00000000..2a560fd1 --- /dev/null +++ b/cmd/neofs-node/config/config.go @@ -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) +} diff --git a/cmd/neofs-node/config/config_test.go b/cmd/neofs-node/config/config_test.go new file mode 100644 index 00000000..231f59b2 --- /dev/null +++ b/cmd/neofs-node/config/config_test.go @@ -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) +} diff --git a/cmd/neofs-node/config/opts.go b/cmd/neofs-node/config/opts.go new file mode 100644 index 00000000..871d9c95 --- /dev/null +++ b/cmd/neofs-node/config/opts.go @@ -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 + } +} diff --git a/cmd/neofs-node/config/test/config.json b/cmd/neofs-node/config/test/config.json new file mode 100644 index 00000000..f10856ac --- /dev/null +++ b/cmd/neofs-node/config/test/config.json @@ -0,0 +1,14 @@ +{ + "value": "some value", + "section": { + "any": "thing" + }, + "string_slice": { + "empty": [], + "filled": [ + "string1", + "string2" + ], + "incorrect": null + } +} diff --git a/cmd/neofs-node/config/test/config.yaml b/cmd/neofs-node/config/test/config.yaml new file mode 100644 index 00000000..e383eae8 --- /dev/null +++ b/cmd/neofs-node/config/test/config.yaml @@ -0,0 +1,13 @@ +value: some value + +section: + any: thing + +string_slice: + empty: [] + + filled: + - string1 + - string2 + + incorrect: \ No newline at end of file diff --git a/cmd/neofs-node/config/util.go b/cmd/neofs-node/config/util.go new file mode 100644 index 00000000..07b43902 --- /dev/null +++ b/cmd/neofs-node/config/util.go @@ -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 +} diff --git a/go.mod b/go.mod index 21a26c12..603072dd 100644 --- a/go.mod +++ b/go.mod @@ -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