-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Reject configuration with unknown fields (#2981)
- Loading branch information
Showing
18 changed files
with
1,040 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
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) | ||
} | ||
} | ||
|
||
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{}{} | ||
} | ||
} | ||
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 "" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
package validate | ||
|
||
import "time" | ||
|
||
type validConfig struct { | ||
Logger struct { | ||
Level string `mapstructure:"level"` | ||
} `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"` | ||
} |
Oops, something went wrong.