Files
ssh-netbox-wrapper/internal/resolver/chain.go
T
Sebastian UnterschützandClaude Fable 5.2 4287b7b650 fix: harden config loading and ssh addresses, stop resolver on API errors
- read netssh.yaml from the user config directory only, not from the
  working directory
- refuse addresses from NetBox or the cache that are neither an IP
  address nor a hostname (ssh read "-oProxyCommand=…" as an option)
- resolver chain stops at an API failure; only "no address" falls
  through to the next strategy
- netssh --help and -h show netssh's help
- start at most one background cache refresh per 5 minutes
- validate the legacy netbox.url
- page through all IP addresses of a host
- CI: run format check, vet, and tests on pull requests and main
- changelog for v0.0.25

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-15 23:25:28 +01:00

67 lines
1.9 KiB
Go

package resolver
import (
"context"
"errors"
"fmt"
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/netbox"
)
// Chain tries each strategy in order until one returns an IP.
type Chain struct {
strategies []Strategy
}
// New builds a Chain from the strategy names listed in the resolver config.
func New(cfg config.ResolverConfig) (*Chain, error) {
var strategies []Strategy
for _, name := range cfg.Strategies {
s, err := newStrategy(name, cfg)
if err != nil {
return nil, fmt.Errorf("resolver strategy %q: %w", name, err)
}
strategies = append(strategies, s)
}
return &Chain{strategies: strategies}, nil
}
func (c *Chain) Resolve(ctx context.Context, entry *netbox.HostEntry, client *netbox.Client) (string, error) {
for _, s := range c.strategies {
ip, err := s.Resolve(ctx, entry, client)
if err == nil {
return ip, nil
}
// Only "this strategy has no address" moves on. After an API failure
// the next strategy would connect to (and cache) a different address
// than the configured order asks for.
if !errors.Is(err, ErrNoIP) {
return "", fmt.Errorf("resolver strategy %q for %q: %w", s.Name(), entry.Name, err)
}
}
return "", fmt.Errorf("no strategy resolved an IP for %q", entry.Name)
}
func newStrategy(name string, cfg config.ResolverConfig) (Strategy, error) {
switch name {
case "primary_ip":
return &PrimaryIPStrategy{}, nil
case "management_subnet":
s, err := NewManagementSubnetStrategy(cfg.ManagementSubnets)
if err != nil {
return nil, err
}
return s, nil
case "hostname":
return &HostnameStrategy{domain: cfg.HostnameDomain}, nil
case "interface_name":
if cfg.InterfaceName == "" {
return nil, fmt.Errorf("interface_name strategy requires resolver.interface_name to be set")
}
return &InterfaceNameStrategy{name: cfg.InterfaceName}, nil
default:
return nil, fmt.Errorf("unknown strategy %q", name)
}
}