- **Shortcuts**: Add hostname normalization with domain stripping and hyphen folding. Include alias generation for cached hosts. - **Shell Hook**: Automate 24h cache refresh trigger with shell startup hook. Add install/uninstall commands for bash, zsh, and fish. - **Wizard**: Extend setup wizard to configure shortcuts (domains, hyphen stripping) and default SSH port. - **Cache**: Add `GetByShortcut` for resolving hosts via normalized shortcuts. Implement `NeedsRefresh` / `SetRefreshed` logic for refresh timestamps. - **Tests**: Comprehensive unit tests for shortcuts, hook installation, cache refresh, and alias generation. - **Docs**: Update README with shortcuts, shell hook, and default SSH port configuration.
This commit is contained in:
@@ -20,10 +20,16 @@ func RunWizard(cfg *config.Config) error {
|
||||
url := cfg.NetBox.URL
|
||||
token := cfg.NetBox.Token
|
||||
defaultUser := cfg.SSH.DefaultUser
|
||||
defaultPort := ""
|
||||
if cfg.SSH.DefaultPort > 0 {
|
||||
defaultPort = strconv.Itoa(cfg.SSH.DefaultPort)
|
||||
}
|
||||
strategiesRaw := strings.Join(cfg.Resolver.Strategies, ", ")
|
||||
subnets := strings.Join(cfg.Resolver.ManagementSubnets, ", ")
|
||||
interfaceName := cfg.Resolver.InterfaceName
|
||||
cacheTTL := strconv.Itoa(cfg.Cache.TTL)
|
||||
shortcutDomains := strings.Join(cfg.Shortcuts.Domains, ", ")
|
||||
stripHyphens := cfg.Shortcuts.StripHyphens
|
||||
|
||||
if strategiesRaw == "" {
|
||||
strategiesRaw = "primary_ip"
|
||||
@@ -62,6 +68,21 @@ func RunWizard(cfg *config.Config) error {
|
||||
Title("Default SSH user").
|
||||
Description("Leave empty to use your system user ($USER).").
|
||||
Value(&defaultUser),
|
||||
huh.NewInput().
|
||||
Title("Default SSH port").
|
||||
Description("Leave empty to use the standard port (22).").
|
||||
Placeholder("22").
|
||||
Value(&defaultPort).
|
||||
Validate(func(s string) error {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return nil
|
||||
}
|
||||
n, err := strconv.Atoi(strings.TrimSpace(s))
|
||||
if err != nil || n < 1 || n > 65535 {
|
||||
return errors.New("must be a port number between 1 and 65535")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
).Title("SSH defaults"),
|
||||
|
||||
huh.NewGroup(
|
||||
@@ -90,6 +111,18 @@ func RunWizard(cfg *config.Config) error {
|
||||
return nil
|
||||
}),
|
||||
).Title("Resolver & cache"),
|
||||
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Domain suffixes").
|
||||
Description("Comma-separated suffixes stripped for shortcuts, e.g. .example.com, .example.de\nAllows typing 'web01' instead of 'web01.example.com'.").
|
||||
Placeholder(".example.com").
|
||||
Value(&shortcutDomains),
|
||||
huh.NewConfirm().
|
||||
Title("Strip hyphens").
|
||||
Description("When enabled, fsn1-web01.example.com can be accessed as fsn1web01.\nOnly works for hosts already in the cache.").
|
||||
Value(&stripHyphens),
|
||||
).Title("Shortcuts"),
|
||||
)
|
||||
|
||||
if err := form.Run(); err != nil {
|
||||
@@ -109,6 +142,11 @@ func RunWizard(cfg *config.Config) error {
|
||||
|
||||
ttl, _ := strconv.Atoi(cacheTTL)
|
||||
|
||||
port := 0
|
||||
if p, err := strconv.Atoi(strings.TrimSpace(defaultPort)); err == nil {
|
||||
port = p
|
||||
}
|
||||
|
||||
var subnetList []string
|
||||
for _, s := range strings.Split(subnets, ",") {
|
||||
if s = strings.TrimSpace(s); s != "" {
|
||||
@@ -116,6 +154,16 @@ func RunWizard(cfg *config.Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
var domainList []string
|
||||
for _, s := range strings.Split(shortcutDomains, ",") {
|
||||
if s = strings.TrimSpace(s); s != "" {
|
||||
if !strings.HasPrefix(s, ".") {
|
||||
s = "." + s
|
||||
}
|
||||
domainList = append(domainList, s)
|
||||
}
|
||||
}
|
||||
|
||||
out := config.Config{
|
||||
NetBox: config.NetBoxConfig{
|
||||
URL: strings.TrimRight(strings.TrimSpace(url), "/"),
|
||||
@@ -124,6 +172,7 @@ func RunWizard(cfg *config.Config) error {
|
||||
},
|
||||
SSH: config.SSHConfig{
|
||||
DefaultUser: strings.TrimSpace(defaultUser),
|
||||
DefaultPort: port,
|
||||
},
|
||||
Resolver: config.ResolverConfig{
|
||||
Strategies: strategies,
|
||||
@@ -133,6 +182,10 @@ func RunWizard(cfg *config.Config) error {
|
||||
Cache: config.CacheConfig{
|
||||
TTL: ttl,
|
||||
},
|
||||
Shortcuts: config.ShortcutsConfig{
|
||||
Domains: domainList,
|
||||
StripHyphens: stripHyphens,
|
||||
},
|
||||
}
|
||||
|
||||
return save(out)
|
||||
@@ -200,6 +253,22 @@ func save(cfg config.Config) error {
|
||||
if cfg.SSH.DefaultUser != "" {
|
||||
fmt.Fprintf(&b, " default_user: %q\n", cfg.SSH.DefaultUser)
|
||||
}
|
||||
if cfg.SSH.DefaultPort > 0 {
|
||||
fmt.Fprintf(&b, " default_port: %d\n", cfg.SSH.DefaultPort)
|
||||
}
|
||||
|
||||
if len(cfg.Shortcuts.Domains) > 0 || cfg.Shortcuts.StripHyphens {
|
||||
b.WriteString("\nshortcuts:\n")
|
||||
if len(cfg.Shortcuts.Domains) > 0 {
|
||||
b.WriteString(" domains:\n")
|
||||
for _, d := range cfg.Shortcuts.Domains {
|
||||
fmt.Fprintf(&b, " - %s\n", d)
|
||||
}
|
||||
}
|
||||
if cfg.Shortcuts.StripHyphens {
|
||||
b.WriteString(" strip_hyphens: true\n")
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, []byte(b.String()), 0o600); err != nil {
|
||||
return fmt.Errorf("writing config: %w", err)
|
||||
|
||||
@@ -116,6 +116,148 @@ func TestSave_CreatesConfigDir(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSave_DefaultPort(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
orig := os.Getenv("XDG_CONFIG_HOME")
|
||||
os.Setenv("XDG_CONFIG_HOME", dir)
|
||||
defer os.Setenv("XDG_CONFIG_HOME", orig)
|
||||
|
||||
cfg := config.Config{
|
||||
NetBox: config.NetBoxConfig{URL: "http://x", Token: "t", TokenVersion: 1},
|
||||
Cache: config.CacheConfig{TTL: 60},
|
||||
SSH: config.SSHConfig{DefaultPort: 2222},
|
||||
}
|
||||
if err := save(cfg); err != nil {
|
||||
t.Fatalf("save: %v", err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(filepath.Join(dir, "netssh.yaml"))
|
||||
if !strings.Contains(string(data), "default_port: 2222") {
|
||||
t.Errorf("expected default_port: 2222 in config, got:\n%s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSave_Shortcuts_WritesSection(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
orig := os.Getenv("XDG_CONFIG_HOME")
|
||||
os.Setenv("XDG_CONFIG_HOME", dir)
|
||||
defer os.Setenv("XDG_CONFIG_HOME", orig)
|
||||
|
||||
cfg := config.Config{
|
||||
NetBox: config.NetBoxConfig{URL: "http://x", Token: "t", TokenVersion: 1},
|
||||
Cache: config.CacheConfig{TTL: 60},
|
||||
Shortcuts: config.ShortcutsConfig{
|
||||
Domains: []string{".example.com"},
|
||||
StripHyphens: true,
|
||||
},
|
||||
}
|
||||
if err := save(cfg); err != nil {
|
||||
t.Fatalf("save: %v", err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(filepath.Join(dir, "netssh.yaml"))
|
||||
content := string(data)
|
||||
for _, want := range []string{"shortcuts:", "domains:", ".example.com", "strip_hyphens: true"} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Errorf("expected %q in config, got:\n%s", want, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSave_OmitsDefaultPortWhenZero(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
orig := os.Getenv("XDG_CONFIG_HOME")
|
||||
os.Setenv("XDG_CONFIG_HOME", dir)
|
||||
defer os.Setenv("XDG_CONFIG_HOME", orig)
|
||||
|
||||
cfg := config.Config{
|
||||
NetBox: config.NetBoxConfig{URL: "http://x", Token: "t", TokenVersion: 1},
|
||||
Cache: config.CacheConfig{TTL: 60},
|
||||
SSH: config.SSHConfig{DefaultPort: 0},
|
||||
}
|
||||
if err := save(cfg); err != nil {
|
||||
t.Fatalf("save: %v", err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(filepath.Join(dir, "netssh.yaml"))
|
||||
if strings.Contains(string(data), "default_port") {
|
||||
t.Errorf("default_port should be omitted when zero, got:\n%s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSave_OmitsShortcutsWhenEmpty(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
orig := os.Getenv("XDG_CONFIG_HOME")
|
||||
os.Setenv("XDG_CONFIG_HOME", dir)
|
||||
defer os.Setenv("XDG_CONFIG_HOME", orig)
|
||||
|
||||
cfg := config.Config{
|
||||
NetBox: config.NetBoxConfig{URL: "http://x", Token: "t", TokenVersion: 1},
|
||||
Cache: config.CacheConfig{TTL: 60},
|
||||
Shortcuts: config.ShortcutsConfig{}, // empty
|
||||
}
|
||||
if err := save(cfg); err != nil {
|
||||
t.Fatalf("save: %v", err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(filepath.Join(dir, "netssh.yaml"))
|
||||
if strings.Contains(string(data), "shortcuts:") {
|
||||
t.Errorf("shortcuts section should be omitted when empty, got:\n%s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSave_Roundtrip_WithNewFields(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
orig := os.Getenv("XDG_CONFIG_HOME")
|
||||
os.Setenv("XDG_CONFIG_HOME", dir)
|
||||
defer os.Setenv("XDG_CONFIG_HOME", orig)
|
||||
|
||||
original := config.Config{
|
||||
NetBox: config.NetBoxConfig{
|
||||
URL: "https://netbox.example.com",
|
||||
Token: "nbt_test",
|
||||
TokenVersion: 2,
|
||||
},
|
||||
SSH: config.SSHConfig{
|
||||
DefaultUser: "admin",
|
||||
DefaultPort: 2222,
|
||||
},
|
||||
Resolver: config.ResolverConfig{
|
||||
Strategies: []string{"primary_ip"},
|
||||
},
|
||||
Cache: config.CacheConfig{TTL: 3600},
|
||||
Shortcuts: config.ShortcutsConfig{
|
||||
Domains: []string{".example.com", ".example.de"},
|
||||
StripHyphens: true,
|
||||
},
|
||||
}
|
||||
|
||||
if err := save(original); err != nil {
|
||||
t.Fatalf("save: %v", err)
|
||||
}
|
||||
|
||||
loaded, err := config.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load after save: %v", err)
|
||||
}
|
||||
|
||||
if loaded.SSH.DefaultPort != original.SSH.DefaultPort {
|
||||
t.Errorf("DefaultPort: got %d, want %d", loaded.SSH.DefaultPort, original.SSH.DefaultPort)
|
||||
}
|
||||
if len(loaded.Shortcuts.Domains) != len(original.Shortcuts.Domains) {
|
||||
t.Errorf("Shortcuts.Domains length: got %d, want %d", len(loaded.Shortcuts.Domains), len(original.Shortcuts.Domains))
|
||||
} else {
|
||||
for i, d := range original.Shortcuts.Domains {
|
||||
if loaded.Shortcuts.Domains[i] != d {
|
||||
t.Errorf("Shortcuts.Domains[%d]: got %q, want %q", i, loaded.Shortcuts.Domains[i], d)
|
||||
}
|
||||
}
|
||||
}
|
||||
if loaded.Shortcuts.StripHyphens != original.Shortcuts.StripHyphens {
|
||||
t.Errorf("StripHyphens: got %v, want %v", loaded.Shortcuts.StripHyphens, original.Shortcuts.StripHyphens)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStrategies(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
|
||||
Reference in New Issue
Block a user