Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reject configuration with unknown fields #2981

Merged
merged 2 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ attribute, which is used for container domain name in NNS contracts (#2954)
- In inner ring config, default ports for TCP addresses are used if they are missing (#2969)
- Metabase is refilled if an object exists in write-cache but is missing in metabase (#2977)
- Pprof and metrics services stop at the end of SN's application lifecycle (#2976)
- Reject configuration with unknown fields (#2981)

### Removed
- Support for node.key configuration (#2959)
Expand Down
106 changes: 106 additions & 0 deletions cmd/internal/configvalidator/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package configvalidator

import (
"errors"
"fmt"
"reflect"
"strings"
)

// ErrUnknowField returns when an unknown field appears in the config.
var ErrUnknowField = errors.New("unknown field")

// CheckForUnknownFields validates the config struct for unknown fields with the config map.
// If the field in the config is a map of a key and some value, need to use `mapstructure:,remain` and
// `prefix` tag with the key prefix, even it is empty string.
func CheckForUnknownFields(configMap map[string]any, config any) error {
return checkForUnknownFields(configMap, config, "")
}

func checkForUnknownFields(configMap map[string]any, config any, currentPath string) error {
expectedFields := getFieldsFromStruct(config)

for key, val := range configMap {
roman-khimov marked this conversation as resolved.
Show resolved Hide resolved
fullPath := key
if currentPath != "" {
fullPath = currentPath + "." + key
}

_, other := expectedFields[",remain"]
_, exists := expectedFields[key]
if !exists && !other {
return fmt.Errorf("%w: %s", ErrUnknowField, fullPath)
}

var fieldVal reflect.Value
if other && !exists {
fieldVal = reflect.ValueOf(config).
FieldByName(getStructFieldByTag(config, "prefix", key, strings.HasPrefix))
} else {
fieldVal = reflect.ValueOf(config).
FieldByName(getStructFieldByTag(config, "mapstructure", key, func(a, b string) bool {
return a == b
}))
}

switch fieldVal.Kind() {
case reflect.Slice:
if fieldVal.Len() == 0 {
return nil
}
fieldVal = fieldVal.Index(0)
case reflect.Map:
fieldVal = fieldVal.MapIndex(reflect.ValueOf(key))
default:
}

if !fieldVal.IsValid() {
return fmt.Errorf("%w: %s", ErrUnknowField, fullPath)
}

nestedMap, okMap := val.(map[string]any)
nestedSlice, okSlice := val.([]any)
if (okMap || okSlice) && fieldVal.Kind() == reflect.Struct {
if okSlice {
for _, slice := range nestedSlice {
if nestedMap, okMap = slice.(map[string]any); okMap {
if err := checkForUnknownFields(nestedMap, fieldVal.Interface(), fullPath); err != nil {
return err
}
}
}
} else if err := checkForUnknownFields(nestedMap, fieldVal.Interface(), fullPath); err != nil {
return err
}
} else if okMap != (fieldVal.Kind() == reflect.Struct) {
return fmt.Errorf("%w: %s", ErrUnknowField, fullPath)
}

Check warning on line 77 in cmd/internal/configvalidator/validate.go

View check run for this annotation

Codecov / codecov/patch

cmd/internal/configvalidator/validate.go#L76-L77

Added lines #L76 - L77 were not covered by tests
}

return nil
}

func getFieldsFromStruct(config any) map[string]struct{} {
fields := make(map[string]struct{})
t := reflect.TypeOf(config)
for i := range t.NumField() {
field := t.Field(i)
if jsonTag := field.Tag.Get("mapstructure"); jsonTag != "" {
fields[jsonTag] = struct{}{}
} else {
fields[field.Name] = struct{}{}
}

Check warning on line 92 in cmd/internal/configvalidator/validate.go

View check run for this annotation

Codecov / codecov/patch

cmd/internal/configvalidator/validate.go#L91-L92

Added lines #L91 - L92 were not covered by tests
}
return fields
}

func getStructFieldByTag(config any, tagKey, tagValue string, comparison func(a, b string) bool) string {
t := reflect.TypeOf(config)
for i := range t.NumField() {
field := t.Field(i)
if jsonTag, ok := field.Tag.Lookup(tagKey); comparison(tagValue, jsonTag) && ok {
return field.Name
}
}
return ""
}
12 changes: 10 additions & 2 deletions cmd/neofs-ir/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"strings"
"time"

"github.com/nspcc-dev/neofs-node/cmd/neofs-ir/internal/validate"
"github.com/spf13/viper"
)

Expand All @@ -29,9 +30,17 @@
v.SetConfigType("yml")
}
err = v.ReadInConfig()
if err != nil {
return nil, err
}

Check warning on line 35 in cmd/neofs-ir/defaults.go

View check run for this annotation

Codecov / codecov/patch

cmd/neofs-ir/defaults.go#L33-L35

Added lines #L33 - L35 were not covered by tests
}

err = validate.ValidateStruct(v)
if err != nil {
return nil, err

Check warning on line 40 in cmd/neofs-ir/defaults.go

View check run for this annotation

Codecov / codecov/patch

cmd/neofs-ir/defaults.go#L38-L40

Added lines #L38 - L40 were not covered by tests
}

return v, err
return v, nil

Check warning on line 43 in cmd/neofs-ir/defaults.go

View check run for this annotation

Codecov / codecov/patch

cmd/neofs-ir/defaults.go#L43

Added line #L43 was not covered by tests
}

func defaultConfiguration(cfg *viper.Viper) {
Expand Down Expand Up @@ -60,7 +69,6 @@
cfg.SetDefault("wallet.address", "") // account address
cfg.SetDefault("wallet.password", "") // password

cfg.SetDefault("timers.emit", "0")
roman-khimov marked this conversation as resolved.
Show resolved Hide resolved
cfg.SetDefault("timers.stop_estimation.mul", 1)
cfg.SetDefault("timers.stop_estimation.div", 4)
cfg.SetDefault("timers.collect_basic_income.mul", 1)
Expand Down
17 changes: 17 additions & 0 deletions cmd/neofs-ir/defaults_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import (
"testing"

"github.com/nspcc-dev/neofs-node/cmd/neofs-ir/internal/validate"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)

func TestValidateDefaultConfig(t *testing.T) {
v := viper.New()

defaultConfiguration(v)

require.NoError(t, validate.ValidateStruct(v))
}
213 changes: 213 additions & 0 deletions cmd/neofs-ir/internal/validate/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package validate

import "time"

type validConfig struct {
roman-khimov marked this conversation as resolved.
Show resolved Hide resolved
Logger struct {
Level string `mapstructure:"level"`
roman-khimov marked this conversation as resolved.
Show resolved Hide resolved
} `mapstructure:"logger"`

Wallet struct {
Path string `mapstructure:"path"`
Address string `mapstructure:"address"`
Password string `mapstructure:"password"`
} `mapstructure:"wallet"`

WithoutMainnet bool `mapstructure:"without_mainnet"`

Morph struct {
DialTimeout time.Duration `mapstructure:"dial_timeout"`
ReconnectionsNumber int `mapstructure:"reconnections_number"`
ReconnectionsDelay time.Duration `mapstructure:"reconnections_delay"`
Endpoints []string `mapstructure:"endpoints"`
Validators []string `mapstructure:"validators"`
Consensus struct {
Magic uint32 `mapstructure:"magic"`
Committee []string `mapstructure:"committee"`

Storage struct {
Type string `mapstructure:"type"`
Path string `mapstructure:"path"`
} `mapstructure:"storage"`

TimePerBlock time.Duration `mapstructure:"time_per_block"`
MaxTraceableBlocks uint32 `mapstructure:"max_traceable_blocks"`
SeedNodes []string `mapstructure:"seed_nodes"`

Hardforks struct {
Name map[string]uint32 `mapstructure:",remain" prefix:""`
} `mapstructure:"hardforks"`

ValidatorsHistory struct {
Height map[string]int `mapstructure:",remain" prefix:""`
} `mapstructure:"validators_history"`

RPC struct {
Listen []string `mapstructure:"listen"`
TLS struct {
Enabled bool `mapstructure:"enabled"`
Listen []string `mapstructure:"listen"`
CertFile string `mapstructure:"cert_file"`
KeyFile string `mapstructure:"key_file"`
} `mapstructure:"tls"`
} `mapstructure:"rpc"`

P2P struct {
DialTimeout time.Duration `mapstructure:"dial_timeout"`
ProtoTickInterval time.Duration `mapstructure:"proto_tick_interval"`
Listen []string `mapstructure:"listen"`
Peers struct {
Min int `mapstructure:"min"`
Max int `mapstructure:"max"`
Attempts int `mapstructure:"attempts"`
} `mapstructure:"peers"`
Ping struct {
Interval time.Duration `mapstructure:"interval"`
Timeout time.Duration `mapstructure:"timeout"`
} `mapstructure:"ping"`
} `mapstructure:"p2p"`
SetRolesInGenesis bool `mapstructure:"set_roles_in_genesis"`
} `mapstructure:"consensus"`
} `mapstructure:"morph"`

FSChainAutodeploy bool `mapstructure:"fschain_autodeploy"`

NNS struct {
SystemEmail string `mapstructure:"system_email"`
} `mapstructure:"nns"`

Mainnet struct {
DialTimeout time.Duration `mapstructure:"dial_timeout"`
ReconnectionsNumber int `mapstructure:"reconnections_number"`
ReconnectionsDelay time.Duration `mapstructure:"reconnections_delay"`
Endpoints []string `mapstructure:"endpoints"`
} `mapstructure:"mainnet"`

Control struct {
AuthorizedKeys []string `mapstructure:"authorized_keys"`
GRPC struct {
Endpoint string `mapstructure:"endpoint"`
} `mapstructure:"grpc"`
} `mapstructure:"control"`

Governance struct {
Disable bool `mapstructure:"disable"`
} `mapstructure:"governance"`

Node struct {
PersistentState struct {
Path string `mapstructure:"path"`
} `mapstructure:"persistent_state"`
} `mapstructure:"node"`

Fee struct {
MainChain int64 `mapstructure:"main_chain"`
SideChain int64 `mapstructure:"side_chain"`
NamedContainerRegister int64 `mapstructure:"named_container_register"`
} `mapstructure:"fee"`

Timers struct {
StopEstimation struct {
Mul int `mapstructure:"mul"`
Div int `mapstructure:"div"`
} `mapstructure:"stop_estimation"`
CollectBasicIncome struct {
Mul int `mapstructure:"mul"`
Div int `mapstructure:"div"`
} `mapstructure:"collect_basic_income"`
DistributeBasicIncome struct {
Mul int `mapstructure:"mul"`
Div int `mapstructure:"div"`
} `mapstructure:"distribute_basic_income"`
} `mapstructure:"timers"`

Emit struct {
Storage struct {
Amount int64 `mapstructure:"amount"`
} `mapstructure:"storage"`
Mint struct {
Value int64 `mapstructure:"value"`
CacheSize int `mapstructure:"cache_size"`
Threshold int `mapstructure:"threshold"`
} `mapstructure:"mint"`
Gas struct {
BalanceThreshold int64 `mapstructure:"balance_threshold"`
} `mapstructure:"gas"`
} `mapstructure:"emit"`

Workers struct {
Alphabet int `mapstructure:"alphabet"`
Balance int `mapstructure:"balance"`
Container int `mapstructure:"container"`
NeoFS int `mapstructure:"neofs"`
Netmap int `mapstructure:"netmap"`
Reputation int `mapstructure:"reputation"`
} `mapstructure:"workers"`

Audit struct {
Timeout struct {
Get time.Duration `mapstructure:"get"`
Head time.Duration `mapstructure:"head"`
RangeHash time.Duration `mapstructure:"rangehash"`
Search time.Duration `mapstructure:"search"`
} `mapstructure:"timeout"`
Task struct {
ExecPoolSize int `mapstructure:"exec_pool_size"`
QueueCapacity int `mapstructure:"queue_capacity"`
} `mapstructure:"task"`
PDP struct {
PairsPoolSize int `mapstructure:"pairs_pool_size"`
MaxSleepInterval time.Duration `mapstructure:"max_sleep_interval"`
} `mapstructure:"pdp"`
POR struct {
PoolSize int `mapstructure:"pool_size"`
} `mapstructure:"por"`
} `mapstructure:"audit"`

Indexer struct {
CacheTimeout time.Duration `mapstructure:"cache_timeout"`
} `mapstructure:"indexer"`

NetmapCleaner struct {
Enabled bool `mapstructure:"enabled"`
Threshold int `mapstructure:"threshold"`
} `mapstructure:"netmap_cleaner"`

Contracts struct {
NeoFS string `mapstructure:"neofs"`
Processing string `mapstructure:"processing"`
Audit string `mapstructure:"audit"`
Balance string `mapstructure:"balance"`
Container string `mapstructure:"container"`
NeoFSID string `mapstructure:"neofsid"`
Netmap string `mapstructure:"netmap"`
Proxy string `mapstructure:"proxy"`
Reputation string `mapstructure:"reputation"`
Alphabet struct {
AZ string `mapstructure:"az"`
Buky string `mapstructure:"buky"`
Vedi string `mapstructure:"vedi"`
Glagoli string `mapstructure:"glagoli"`
Dobro string `mapstructure:"dobro"`
Yest string `mapstructure:"yest"`
Zhivete string `mapstructure:"zhivete"`
} `mapstructure:"alphabet"`
} `mapstructure:"contracts"`

Pprof struct {
Enabled bool `mapstructure:"enabled"`
Address string `mapstructure:"address"`
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"`
} `mapstructure:"pprof"`

Prometheus struct {
Enabled bool `mapstructure:"enabled"`
Address string `mapstructure:"address"`
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"`
} `mapstructure:"prometheus"`

Settlement struct {
BasicIncomeRate int64 `mapstructure:"basic_income_rate"`
AuditFee int64 `mapstructure:"audit_fee"`
} `mapstructure:"settlement"`
}
Loading
Loading