Files
ssh-netbox-wrapper/internal/config/config.go
T
2026-10-27 18:10:44 +01:00

134 lines
3.8 KiB
Go

package config
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/spf13/viper"
)
type Config struct {
Shell ShellConfig `mapstructure:"shell"`
NetBox NetBoxConfig `mapstructure:"netbox"`
Resolver ResolverConfig `mapstructure:"resolver"`
Cache CacheConfig `mapstructure:"cache"`
SSH SSHConfig `mapstructure:"ssh"`
Shortcuts ShortcutsConfig `mapstructure:"shortcuts"`
}
type ShellConfig struct {
Alias string `mapstructure:"alias"`
}
type NetBoxConfig struct {
URL string `mapstructure:"url"`
Token string `mapstructure:"token"`
TokenVersion int `mapstructure:"token_version"`
}
type ResolverConfig struct {
Strategies []string `mapstructure:"strategies"`
ManagementSubnets []string `mapstructure:"management_subnets"`
InterfaceName string `mapstructure:"interface_name"`
}
type CacheConfig struct {
TTL int `mapstructure:"ttl"`
Path string `mapstructure:"path"`
}
type SSHConfig struct {
DefaultUser string `mapstructure:"default_user"`
DefaultPort int `mapstructure:"default_port"`
}
type ShortcutsConfig struct {
Domains []string `mapstructure:"domains"`
StripHyphens bool `mapstructure:"strip_hyphens"`
}
// Path returns the canonical config file path.
func Path() string {
configDir, err := os.UserConfigDir()
if err != nil {
configDir = filepath.Join(os.Getenv("HOME"), ".config")
}
return filepath.Join(configDir, "netssh.yaml")
}
func Load() (*Config, error) {
v := viper.New()
v.SetDefault("resolver.strategies", []string{"management_subnet", "primary_ip"})
v.SetDefault("resolver.management_subnets", []string{})
v.SetDefault("cache.ttl", 3600)
configDir, err := os.UserConfigDir()
if err == nil {
v.SetConfigName("netssh")
v.SetConfigType("yaml")
v.AddConfigPath(filepath.Join(configDir))
v.AddConfigPath(".")
}
v.SetEnvPrefix("NETSSH")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
for _, key := range []string{"netbox.url", "netbox.token", "netbox.token_version", "resolver.strategies", "resolver.management_subnets", "resolver.interface_name", "cache.ttl", "cache.path", "ssh.default_user", "ssh.default_port", "shortcuts.domains", "shortcuts.strip_hyphens", "shell.alias"} {
if err := v.BindEnv(key); err != nil {
return nil, err
}
}
if err := v.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, fmt.Errorf("reading config: %w", err)
}
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("parsing config: %w", err)
}
if err := ValidateCommandAlias(cfg.Shell.Alias); err != nil {
return nil, err
}
if cfg.NetBox.TokenVersion == 0 && cfg.NetBox.Token != "" {
if strings.HasPrefix(cfg.NetBox.Token, "nbt_") {
cfg.NetBox.TokenVersion = 2
} else {
cfg.NetBox.TokenVersion = 1
}
}
if cfg.Cache.Path == "" {
cacheDir, err := os.UserCacheDir()
if err != nil {
cacheDir = filepath.Join(os.Getenv("HOME"), ".cache")
}
cfg.Cache.Path = filepath.Join(cacheDir, "netssh", "hosts.json")
}
return &cfg, nil
}
// ValidateCommandAlias restricts aliases to portable shell identifiers.
func ValidateCommandAlias(name string) error {
if name == "" {
return nil
}
if !regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`).MatchString(name) {
return fmt.Errorf("shell.alias must be a shell identifier (e.g. j)")
}
switch name {
case "netssh", "alias", "unalias", "source", "eval", "exec", "command", "builtin", "if", "then", "else", "elif", "fi", "for", "while", "until", "do", "done", "case", "esac", "in", "function", "select", "time", "coproc", "end", "begin", "switch", "set", "export", "return", "break", "continue", "not", "and", "or":
return fmt.Errorf("shell.alias %q is reserved; choose another name", name)
}
return nil
}