Refuse config keys that no setting has (closes #419)
check / check (push) Successful in 9m9s

config.Parse walks the file as written and fails on the first key that
matches no setting, naming its dotted path and line, so a typo in
exclude or prune_grace_period no longer falls back silently. Keys that a
${...} reference brings in are checked once smartconfig has filled it
in, and named without a line. YAML merge keys and a top-level env
section, which smartconfig reads, still load; an anchor that contains
itself is an error, and each anchored map is checked once per type.
config set refuses such a key before writing, with the same error, and
sets a key under env like any other value. The key-to-field lookup
moves from internal/cli to config.SettingType so both use it.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-11 03:01:43 +00:00
1 parent 88a2aa7953
commit c40ff647c3
6 files changed
+462 -47

No files matched your search

+14 -38
View File
@@ -443,11 +443,10 @@ number, otherwise as a map.
If the config loads, a change after which it would not, such as
compression_level 99, is refused and the file is left unchanged. A
config that does not load, such as the one config init writes before
storage_url is set or one with a ${ENV:...} reference to a variable
not set here, is checked one setting at a time: only a change after
which the changed setting cannot be read, such as one giving it the
wrong type, is refused.
config that does not load is checked one setting at a time. Examples
are the file config init writes before storage_url is set, and one
with a ${ENV:...} reference to a variable not set in this shell. In
such a config, an unknown key or a value of the wrong type is refused.
Examples:
vaultik config set storage_url "file:///mnt/backups"
@@ -484,6 +483,13 @@ func writeConfigSet(out *ui.Writer, path, key, value string) error {
keys := strings.Split(key, ".")
if keys[0] != config.EnvSection {
_, err = config.SettingType(keys)
if err != nil {
return err
}
}
err = yamlPathSet(root, keys, value)
if err != nil {
return err
@@ -739,41 +745,11 @@ func yamlPathSet(root *yaml.Node, keys []string, value string) error {
}
// configKeyIsString reports whether the dotted key names a string in
// config.Config, following the fields' yaml tags, as s3.access_key_id and
// snapshots.home.exclude.0 do.
// config.Config, as s3.access_key_id and snapshots.home.exclude.0 do.
func configKeyIsString(keys []string) bool {
typ := reflect.TypeFor[config.Config]()
typ, err := config.SettingType(keys)
for _, key := range keys {
switch {
case typ.Kind() == reflect.Map || typ.Kind() == reflect.Slice:
// The key is a snapshot name or a list index.
typ = typ.Elem()
case typ.Kind() == reflect.Struct:
field, ok := yamlField(typ, key)
if !ok {
return false
}
typ = field.Type
default:
return false
}
}
return typ.Kind() == reflect.String
}
// yamlField returns the field of struct type typ whose yaml tag names key.
func yamlField(typ reflect.Type, key string) (reflect.StructField, bool) {
for field := range typ.Fields() {
name, _, _ := strings.Cut(field.Tag.Get("yaml"), ",")
if name == key {
return field, true
}
}
return reflect.StructField{}, false
return err == nil && typ.Kind() == reflect.String
}
// yamlSetInMapping resolves (creating if needed) the value node for the