- 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>
67 lines
1.9 KiB
Go
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)
|
|
}
|
|
}
|