feat(configure): edit every config option in the configurator

After the NetBox instances, netssh configure now opens a menu with all
sections: SSH defaults, global SSH options (jump host, host key
checking, target, address family, extra args, -o options), SSH rules,
resolver & cache (including the cache file), shortcuts & command alias,
and completion.max_results. SSH options can also be set per instance.

The wizard rebuilt the config from its own fields, so re-running it
dropped global SSH options, ssh.rules and completion.max_results from
the file. It now edits a copy of the loaded config, and save validates
SSH options and rules before writing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sebastian UnterschützandClaude Opus 5.5 committed 2026-11-13 22:42:13 +01:00
1 parent ac713ac97b
commit d51c040d9f
8 files changed
+630 -174

No files matched your search

+43
View File
@@ -455,3 +455,46 @@ func TestSaveKeepsSSHOptions(t *testing.T) {
t.Fatalf("ssh settings lost:\n%+v\n%+v", loaded.SSH, loaded.NetBoxes[0].SSH)
}
}
// Re-running the configurator must keep settings that have no form field of
// their own or were not touched in the menu.
func TestPrepareKeepsUnmanagedSettings(t *testing.T) {
cfg := config.Config{
NetBox: config.NetBoxConfig{URL: "https://legacy.example", Token: "t"},
SSH: config.SSHConfig{
DefaultUser: "root",
SSHOptions: config.SSHOptions{ProxyJump: "jh.example.com", Options: map[string]string{"forwardagent": "yes"}},
Rules: []config.SSHRule{{Match: config.SSHMatch{Tag: "dmz"}, SSHOptions: config.SSHOptions{ProxyJump: "none"}}},
},
Completion: config.CompletionConfig{MaxResults: 50},
Cache: config.CacheConfig{Path: "/var/cache/netssh/hosts.json"},
}
sources := []config.NetBoxConfig{{Name: "default", URL: "https://legacy.example", Token: "t"}}
got := prepare(cfg, sources)
if !reflect.DeepEqual(got.SSH, cfg.SSH) || got.Completion != cfg.Completion || got.Cache.Path != cfg.Cache.Path {
t.Fatalf("settings lost: %+v", got)
}
if got.NetBox.URL != "" || !reflect.DeepEqual(got.NetBoxes, sources) {
t.Fatalf("legacy instance not migrated: %+v / %+v", got.NetBox, got.NetBoxes)
}
if !reflect.DeepEqual(got.Resolver.Strategies, []string{"primary_ip"}) || got.Cache.TTL != 3600 {
t.Fatalf("defaults missing: %+v %+v", got.Resolver, got.Cache)
}
}
func TestSaveRejectsInvalidSSHOptions(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
t.Setenv("HOME", t.TempDir())
cfg := config.Config{
NetBoxes: []config.NetBoxConfig{{Name: "prod", URL: "https://prod.example", Token: "t"}},
Resolver: config.ResolverConfig{Strategies: []string{"primary_ip"}},
SSH: config.SSHConfig{Rules: []config.SSHRule{{SSHOptions: config.SSHOptions{ProxyJump: "none"}}}},
}
if err := save(cfg); err == nil {
t.Fatal("rule without match was saved")
}
if _, err := os.Stat(config.Path()); !os.IsNotExist(err) {
t.Fatalf("config file written despite error: %v", err)
}
}