feat: validate configuration on startup, fail fast on bad config (closes #52) #53
@@ -132,10 +132,12 @@ func newFromSmartConfig(sc *smartconfig.Config) (*Config, error) {
|
||||
|
||||
// validateKnownKeys rejects configuration files containing keys the
|
||||
// application does not understand, so typos fail at startup instead of
|
||||
// being silently ignored. The env section is permitted because
|
||||
// being silently ignored, and rejects keys that are explicitly set to
|
||||
// null: a null is a SET value, never an omission, so it must not
|
||||
// silently take the default. The env section is permitted because
|
||||
// smartconfig consumes it for environment variable injection.
|
||||
func validateKnownKeys(sc *smartconfig.Config) error {
|
||||
var unknown []string
|
||||
var unknown, nullKeys []string
|
||||
|
||||
for key, value := range sc.Data() {
|
||||
if !isKnownConfigKey(key) {
|
||||
@@ -144,6 +146,12 @@ func validateKnownKeys(sc *smartconfig.Config) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
nullKeys = append(nullKeys, key)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if key == "metrics" {
|
||||
metricsMap, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
@@ -152,9 +160,15 @@ func validateKnownKeys(sc *smartconfig.Config) error {
|
||||
"metrics", value)
|
||||
}
|
||||
|
||||
for subkey := range metricsMap {
|
||||
for subkey, subvalue := range metricsMap {
|
||||
if subkey != "username" && subkey != "password" {
|
||||
unknown = append(unknown, "metrics."+subkey)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if subvalue == nil {
|
||||
nullKeys = append(nullKeys, "metrics."+subkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,9 +180,29 @@ func validateKnownKeys(sc *smartconfig.Config) error {
|
||||
return fmt.Errorf("unknown config keys: %s", strings.Join(unknown, ", "))
|
||||
}
|
||||
|
||||
if len(nullKeys) > 0 {
|
||||
sort.Strings(nullKeys)
|
||||
|
||||
if len(nullKeys) == 1 {
|
||||
return errNullConfigValue(nullKeys[0])
|
||||
}
|
||||
|
||||
return fmt.Errorf(
|
||||
"config keys %s: value is null; omit a key entirely to use its default",
|
||||
strings.Join(nullKeys, ", "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// errNullConfigValue reports a config key that is explicitly set to
|
||||
// null (including the bare "key:" form and the "~" alias). Silently
|
||||
// applying the default would mask a truncated or typo'd config entry.
|
||||
func errNullConfigValue(key string) error {
|
||||
return fmt.Errorf(
|
||||
"config key %q: value is null; omit the key entirely to use the default", key)
|
||||
}
|
||||
|
||||
// isKnownConfigKey reports whether key is a permitted top-level
|
||||
// configuration key.
|
||||
func isKnownConfigKey(key string) bool {
|
||||
@@ -371,17 +405,22 @@ func (l *strictLoader) boolVal(key string, defaultVal bool) bool {
|
||||
}
|
||||
|
||||
// getString returns the string value for key, or defaultVal if the key
|
||||
// is omitted. A present value that is not a string is an error.
|
||||
// is omitted. A present value that is not a string, or is explicitly
|
||||
// null, is an error.
|
||||
func getString(sc *smartconfig.Config, key, defaultVal string) (string, error) {
|
||||
if sc == nil {
|
||||
return defaultVal, nil
|
||||
}
|
||||
|
||||
raw, ok := sc.Get(key)
|
||||
if !ok || raw == nil {
|
||||
if !ok {
|
||||
return defaultVal, nil
|
||||
}
|
||||
|
||||
if raw == nil {
|
||||
return "", errNullConfigValue(key)
|
||||
}
|
||||
|
||||
str, ok := raw.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("config key %q: value %v (%T) is not a string",
|
||||
@@ -392,18 +431,22 @@ func getString(sc *smartconfig.Config, key, defaultVal string) (string, error) {
|
||||
}
|
||||
|
||||
// getInt returns the integer value for key, or defaultVal if the key is
|
||||
// omitted. A present value that is not a whole number is an error;
|
||||
// fractional values are never truncated.
|
||||
// omitted. A present value that is not a whole number, or is explicitly
|
||||
// null, is an error; fractional values are never truncated.
|
||||
func getInt(sc *smartconfig.Config, key string, defaultVal int) (int, error) {
|
||||
if sc == nil {
|
||||
return defaultVal, nil
|
||||
}
|
||||
|
||||
raw, ok := sc.Get(key)
|
||||
if !ok || raw == nil {
|
||||
if !ok {
|
||||
return defaultVal, nil
|
||||
}
|
||||
|
||||
if raw == nil {
|
||||
return 0, errNullConfigValue(key)
|
||||
}
|
||||
|
||||
switch val := raw.(type) {
|
||||
case int:
|
||||
return val, nil
|
||||
@@ -430,17 +473,22 @@ func getInt(sc *smartconfig.Config, key string, defaultVal int) (int, error) {
|
||||
|
||||
// getBool returns the boolean value for key, or defaultVal if the key
|
||||
// is omitted. A present value that is not a boolean (or a ParseBool-able
|
||||
// string) is an error; numbers are not accepted as booleans.
|
||||
// string), or is explicitly null, is an error; numbers are not accepted
|
||||
// as booleans.
|
||||
func getBool(sc *smartconfig.Config, key string, defaultVal bool) (bool, error) {
|
||||
if sc == nil {
|
||||
return defaultVal, nil
|
||||
}
|
||||
|
||||
raw, ok := sc.Get(key)
|
||||
if !ok || raw == nil {
|
||||
if !ok {
|
||||
return defaultVal, nil
|
||||
}
|
||||
|
||||
if raw == nil {
|
||||
return false, errNullConfigValue(key)
|
||||
}
|
||||
|
||||
switch val := raw.(type) {
|
||||
case bool:
|
||||
return val, nil
|
||||
@@ -459,17 +507,21 @@ func getBool(sc *smartconfig.Config, key string, defaultVal bool) (bool, error)
|
||||
|
||||
// validateAllowlistHostsValue checks the raw shape of the
|
||||
// allowlist_hosts value before the lenient extraction in getStringSlice
|
||||
// runs: a value that is not a list of strings (or a comma-separated
|
||||
// string), a non-string entry, or an empty entry is an error, never
|
||||
// silently skipped.
|
||||
// runs: an explicitly null value, a value that is not a list of strings
|
||||
// (or a comma-separated string), a non-string entry, or an empty entry
|
||||
// is an error, never silently skipped.
|
||||
func validateAllowlistHostsValue(sc *smartconfig.Config) error {
|
||||
const key = "allowlist_hosts"
|
||||
|
||||
raw, ok := sc.Get(key)
|
||||
if !ok || raw == nil {
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if raw == nil {
|
||||
return errNullConfigValue(key)
|
||||
}
|
||||
|
||||
switch val := raw.(type) {
|
||||
case []interface{}:
|
||||
for _, item := range val {
|
||||
|
||||
Reference in New Issue
Block a user