Refuse config keys that no setting has (closes #419)
check / check (push) Successful in 14m3s
check / check (push) Successful in 14m3s
A misspelled or unknown config key was accepted silently, so a typo in exclude backed up everything it meant to leave out. Loading now refuses a key that no setting has, at any depth, naming its dotted path and, when it is in the file, its line; keys brought in by ${...} references are checked too. config set refuses an unknown key. A top-level env: section stays accepted, since the config library uses it. Anchors that contain themselves fail with an error.
Model: opus-5-5
This commit was merged in pull request #443.
This commit is contained in:
1 parent
028a166256
commit
d8d13d1c8b
6 files changed
+462
-47
No files matched your search
+14
-38
@@ -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
|
||||
|
||||
@@ -225,7 +225,7 @@ func TestConfigSetIndexUnderMissingKey(t *testing.T) {
|
||||
// change after which the config would not load, with an error giving the
|
||||
// reason, and leaves the file as it was. On the file `config init` writes,
|
||||
// which does not load until storage_url is set, and on one that cannot be
|
||||
// read here, only a value of the wrong type is refused.
|
||||
// read here, only an unknown key or a value of the wrong type is refused.
|
||||
func TestConfigSetKeepsFileThatWouldNotLoad(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -237,6 +237,14 @@ func TestConfigSetKeepsFileThatWouldNotLoad(t *testing.T) {
|
||||
value string
|
||||
reason string
|
||||
}{
|
||||
// Keys that no setting has, on a config that loads and on one
|
||||
// that does not.
|
||||
{configWithoutRecipients, "nosuchkey", "1",
|
||||
`unknown config key "nosuchkey"`},
|
||||
{configWithoutRecipients, "snapshots.home.exlude", "/tmp",
|
||||
`unknown config key "snapshots.home.exlude"`},
|
||||
{defaultConfigText(macOS), "compression_lvl", "9",
|
||||
`unknown config key "compression_lvl"`},
|
||||
// Index 1 of the empty list the missing key gives.
|
||||
{configWithoutRecipients, "age_recipients.1", testRecipient,
|
||||
"index out of range"},
|
||||
@@ -335,6 +343,39 @@ func TestConfigSetRefusesOnlyItsOwnFailure(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigSetWritesEnvSection checks that config set writes a key under
|
||||
// the top-level env section, whose keys are not settings, on a config that
|
||||
// loads.
|
||||
func TestConfigSetWritesEnvSection(t *testing.T) {
|
||||
const name = "VAULTIK_TEST_CONFIG_SET_ENV"
|
||||
|
||||
// Loading the changed config sets the variable; t.Setenv restores it
|
||||
// after the test.
|
||||
t.Setenv(name, "")
|
||||
|
||||
path := filepath.Join(t.TempDir(), "config.yml")
|
||||
|
||||
err := os.WriteFile(path, []byte(configWithoutRecipients), configFileMode)
|
||||
if err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
err = writeConfigSet(ui.NewWithColor(&bytes.Buffer{}, false),
|
||||
path, "env."+name, "value")
|
||||
if err != nil {
|
||||
t.Fatalf("config set env.%s: %v", name, err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path) //nolint:gosec // G304: test-controlled path
|
||||
if err != nil {
|
||||
t.Fatalf("read config: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(data), "env:\n "+name+": value\n") {
|
||||
t.Errorf("config set env.%s wrote:\n%s", name, data)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigSetStringLooksLikeNumber sets string settings to values that
|
||||
// YAML reads as numbers or booleans when they are unquoted, and checks that
|
||||
// config.Load returns each one unchanged.
|
||||
|
||||
Reference in new issue
Block a user