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>
This commit is contained in:
1 parent
6e80b733dc
commit
4287b7b650
19 files changed
+317
-30
No files matched your search
@@ -2,6 +2,7 @@ package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
|
||||
@@ -32,6 +33,12 @@ func (c *Chain) Resolve(ctx context.Context, entry *netbox.HostEntry, client *ne
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -153,3 +153,17 @@ func TestNew_MultipleStrategies(t *testing.T) {
|
||||
t.Errorf("got %d strategies, want 2", len(c.strategies))
|
||||
}
|
||||
}
|
||||
|
||||
// An API failure must not fall through: the next strategy would connect to
|
||||
// and cache a different address than the configured order asks for.
|
||||
func TestChain_APIErrorStopsTheChain(t *testing.T) {
|
||||
apiErr := errors.New("connection refused")
|
||||
c := &Chain{strategies: []Strategy{
|
||||
&stubStrategy{name: "management_subnet", err: apiErr},
|
||||
&stubStrategy{name: "primary_ip", ip: "10.0.0.2"},
|
||||
}}
|
||||
ip, err := c.Resolve(context.Background(), &netbox.HostEntry{Name: "host"}, nil)
|
||||
if !errors.Is(err, apiErr) || ip != "" {
|
||||
t.Fatalf("got %q, %v; want the API error and no address", ip, err)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/netbox"
|
||||
@@ -31,6 +33,10 @@ func (s *InterfaceNameStrategy) Resolve(ctx context.Context, entry *netbox.HostE
|
||||
}
|
||||
|
||||
ips, err := client.GetIPsWithFilterForHost(ctx, *entry, filterParam)
|
||||
var status *netbox.StatusError
|
||||
if errors.As(err, &status) && status.Code == http.StatusBadRequest {
|
||||
return "", ErrNoIP // the host has no interface of this name
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetching IPs for interface %q: %w", s.name, err)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@@ -54,3 +55,16 @@ func urlQuery(s string) (string, error) {
|
||||
q, err := url.ParseQuery(s)
|
||||
return q.Encode(), err
|
||||
}
|
||||
|
||||
// Only the HTTP 400 of a missing interface means "no address here"; a server
|
||||
// error must stop the chain instead of silently using the next strategy.
|
||||
func TestInterfaceNameStrategyServerErrorIsNotNoIP(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
}))
|
||||
defer srv.Close()
|
||||
_, err := (&InterfaceNameStrategy{name: "mgmt0"}).Resolve(context.Background(), &netbox.HostEntry{ID: 7, Kind: "device"}, netbox.NewClient(srv.URL, "t", 1))
|
||||
if err == nil || errors.Is(err, ErrNoIP) {
|
||||
t.Fatalf("got %v, want an API error", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user