From 942313a10ae84ef6ade7fb96ef53ce74bb5b1ed8 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 28 Apr 2026 13:32:48 +0000 Subject: [PATCH] types: move DebugRoutes from routes to types Unblocks deletion of the routes package. Updates #3203 --- hscontrol/types/debug_routes.go | 22 ++++++++++++++++++++++ integration/control.go | 3 +-- integration/hsic/hsic.go | 5 ++--- integration/route_test.go | 29 ++++++++++++++--------------- 4 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 hscontrol/types/debug_routes.go diff --git a/hscontrol/types/debug_routes.go b/hscontrol/types/debug_routes.go new file mode 100644 index 00000000..c08425be --- /dev/null +++ b/hscontrol/types/debug_routes.go @@ -0,0 +1,22 @@ +package types + +import "net/netip" + +// DebugRoutes is the JSON-shaped snapshot of the headscale primary +// route ledger exposed by the /debug/routes endpoint and consumed by +// the integration test harness. It used to live in hscontrol/routes, +// but the algorithm now runs inside hscontrol/state and that package +// must not be imported from integration code. +type DebugRoutes struct { + // AvailableRoutes maps node IDs to their advertised routes + // (intersection of announced and approved). Only nodes currently + // connected to headscale are listed. + AvailableRoutes map[NodeID][]netip.Prefix `json:"available_routes"` + + // PrimaryRoutes maps route prefixes to the node currently elected + // primary for that prefix. + PrimaryRoutes map[string]NodeID `json:"primary_routes"` + + // UnhealthyNodes lists nodes that have failed health probes. + UnhealthyNodes []NodeID `json:"unhealthy_nodes,omitempty"` +} diff --git a/integration/control.go b/integration/control.go index d9273ae6..8c3cdbf0 100644 --- a/integration/control.go +++ b/integration/control.go @@ -6,7 +6,6 @@ import ( v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/juanfont/headscale/hscontrol" policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2" - "github.com/juanfont/headscale/hscontrol/routes" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/integration/hsic" "github.com/ory/dockertest/v3" @@ -43,7 +42,7 @@ type ControlServer interface { GetIPInNetwork(network *dockertest.Network) string SetPolicy(pol *policyv2.Policy) error GetAllMapReponses() (map[types.NodeID][]tailcfg.MapResponse, error) - PrimaryRoutes() (*routes.DebugRoutes, error) + PrimaryRoutes() (*types.DebugRoutes, error) DebugBatcher() (*hscontrol.DebugBatcherInfo, error) DebugNodeStore() (map[types.NodeID]types.Node, error) DebugFilter() ([]tailcfg.FilterRule, error) diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index 4f2e59be..2974998f 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -27,7 +27,6 @@ import ( v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/juanfont/headscale/hscontrol" policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2" - "github.com/juanfont/headscale/hscontrol/routes" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/integration/dockertestutil" @@ -1639,7 +1638,7 @@ func (t *HeadscaleInContainer) GetAllMapReponses() (map[types.NodeID][]tailcfg.M } // PrimaryRoutes fetches the primary routes from the debug endpoint. -func (t *HeadscaleInContainer) PrimaryRoutes() (*routes.DebugRoutes, error) { +func (t *HeadscaleInContainer) PrimaryRoutes() (*types.DebugRoutes, error) { // Execute curl inside the container to access the debug endpoint locally command := []string{ "curl", "-s", "-H", "Accept: application/json", "http://localhost:9090/debug/routes", @@ -1650,7 +1649,7 @@ func (t *HeadscaleInContainer) PrimaryRoutes() (*routes.DebugRoutes, error) { return nil, fmt.Errorf("fetching routes from debug endpoint: %w", err) } - var debugRoutes routes.DebugRoutes + var debugRoutes types.DebugRoutes if err := json.Unmarshal([]byte(result), &debugRoutes); err != nil { //nolint:noinlineerr return nil, fmt.Errorf("decoding routes response: %w", err) } diff --git a/integration/route_test.go b/integration/route_test.go index 358540b7..e660dd35 100644 --- a/integration/route_test.go +++ b/integration/route_test.go @@ -17,7 +17,6 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2" - "github.com/juanfont/headscale/hscontrol/routes" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/integration/hsic" @@ -227,7 +226,7 @@ func TestHASubnetRouterFailover(t *testing.T) { propagationTime := integrationutil.ScaledTimeout(60 * time.Second) // Helper function to validate primary routes table state - validatePrimaryRoutes := func(t *testing.T, headscale ControlServer, expectedRoutes *routes.DebugRoutes, message string) { + validatePrimaryRoutes := func(t *testing.T, headscale ControlServer, expectedRoutes *types.DebugRoutes, message string) { t.Helper() assert.EventuallyWithT(t, func(c *assert.CollectT) { primaryRoutesState, err := headscale.PrimaryRoutes() @@ -383,7 +382,7 @@ func TestHASubnetRouterFailover(t *testing.T) { } // Validate primary routes table state - no routes approved yet - validatePrimaryRoutes(t, headscale, &routes.DebugRoutes{ + validatePrimaryRoutes(t, headscale, &types.DebugRoutes{ AvailableRoutes: map[types.NodeID][]netip.Prefix{}, PrimaryRoutes: map[string]types.NodeID{}, // No primary routes yet }, "Primary routes table should be empty (no approved routes yet)") @@ -477,7 +476,7 @@ func TestHASubnetRouterFailover(t *testing.T) { }, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 1") // Validate primary routes table state - router 1 is primary - validatePrimaryRoutes(t, headscale, &routes.DebugRoutes{ + validatePrimaryRoutes(t, headscale, &types.DebugRoutes{ AvailableRoutes: map[types.NodeID][]netip.Prefix{ types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref}, // Note: Router 2 and 3 are available but not approved @@ -557,7 +556,7 @@ func TestHASubnetRouterFailover(t *testing.T) { }, propagationTime, 200*time.Millisecond, "Verifying Router 1 remains PRIMARY after Router 2 approval") // Validate primary routes table state - router 1 still primary, router 2 approved but standby - validatePrimaryRoutes(t, headscale, &routes.DebugRoutes{ + validatePrimaryRoutes(t, headscale, &types.DebugRoutes{ AvailableRoutes: map[types.NodeID][]netip.Prefix{ types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref}, types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref}, @@ -594,7 +593,7 @@ func TestHASubnetRouterFailover(t *testing.T) { }, propagationTime, 200*time.Millisecond, "Verifying traceroute still goes through router 1 in HA mode") // Validate primary routes table state - router 1 primary, router 2 approved (standby) - validatePrimaryRoutes(t, headscale, &routes.DebugRoutes{ + validatePrimaryRoutes(t, headscale, &types.DebugRoutes{ AvailableRoutes: map[types.NodeID][]netip.Prefix{ types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref}, types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref}, @@ -704,7 +703,7 @@ func TestHASubnetRouterFailover(t *testing.T) { }, propagationTime, 200*time.Millisecond, "Verifying traffic still flows through PRIMARY router 1 with full HA setup active") // Validate primary routes table state - all 3 routers approved, router 1 still primary - validatePrimaryRoutes(t, headscale, &routes.DebugRoutes{ + validatePrimaryRoutes(t, headscale, &types.DebugRoutes{ AvailableRoutes: map[types.NodeID][]netip.Prefix{ types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref}, types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref}, @@ -785,7 +784,7 @@ func TestHASubnetRouterFailover(t *testing.T) { }, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 2 after failover") // Validate primary routes table state - router 2 is now primary after router 1 failure - validatePrimaryRoutes(t, headscale, &routes.DebugRoutes{ + validatePrimaryRoutes(t, headscale, &types.DebugRoutes{ AvailableRoutes: map[types.NodeID][]netip.Prefix{ // Router 1 is disconnected, so not in AvailableRoutes types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref}, @@ -859,7 +858,7 @@ func TestHASubnetRouterFailover(t *testing.T) { }, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 3 after second failover") // Validate primary routes table state - router 3 is now primary after router 2 failure - validatePrimaryRoutes(t, headscale, &routes.DebugRoutes{ + validatePrimaryRoutes(t, headscale, &types.DebugRoutes{ AvailableRoutes: map[types.NodeID][]netip.Prefix{ // Routers 1 and 2 are disconnected, so not in AvailableRoutes types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref}, @@ -939,7 +938,7 @@ func TestHASubnetRouterFailover(t *testing.T) { }, propagationTime, 200*time.Millisecond, "Verifying traceroute still goes through router 3 after router 1 recovery") // Validate primary routes table state - router 3 remains primary after router 1 comes back - validatePrimaryRoutes(t, headscale, &routes.DebugRoutes{ + validatePrimaryRoutes(t, headscale, &types.DebugRoutes{ AvailableRoutes: map[types.NodeID][]netip.Prefix{ types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref}, // Router 2 is still disconnected @@ -1022,7 +1021,7 @@ func TestHASubnetRouterFailover(t *testing.T) { }, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 3 after full recovery") // Validate primary routes table state - router 3 remains primary after all routers back online - validatePrimaryRoutes(t, headscale, &routes.DebugRoutes{ + validatePrimaryRoutes(t, headscale, &types.DebugRoutes{ AvailableRoutes: map[types.NodeID][]netip.Prefix{ types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref}, types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref}, @@ -1109,7 +1108,7 @@ func TestHASubnetRouterFailover(t *testing.T) { }, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 1 after route disable") // Validate primary routes table state - router 1 is primary after router 3 route disabled - validatePrimaryRoutes(t, headscale, &routes.DebugRoutes{ + validatePrimaryRoutes(t, headscale, &types.DebugRoutes{ AvailableRoutes: map[types.NodeID][]netip.Prefix{ types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref}, types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref}, @@ -1197,7 +1196,7 @@ func TestHASubnetRouterFailover(t *testing.T) { }, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 2 after second route disable") // Validate primary routes table state - router 2 is primary after router 1 route disabled - validatePrimaryRoutes(t, headscale, &routes.DebugRoutes{ + validatePrimaryRoutes(t, headscale, &types.DebugRoutes{ AvailableRoutes: map[types.NodeID][]netip.Prefix{ // Router 1's route is no longer approved, so not in AvailableRoutes types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref}, @@ -1284,7 +1283,7 @@ func TestHASubnetRouterFailover(t *testing.T) { }, propagationTime, 200*time.Millisecond, "Verifying traceroute still goes through router 2 after route re-enable") // Validate primary routes table state after router 1 re-approval - validatePrimaryRoutes(t, headscale, &routes.DebugRoutes{ + validatePrimaryRoutes(t, headscale, &types.DebugRoutes{ AvailableRoutes: map[types.NodeID][]netip.Prefix{ types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref}, types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref}, @@ -1326,7 +1325,7 @@ func TestHASubnetRouterFailover(t *testing.T) { }, propagationTime, 200*time.Millisecond, "Waiting for route state after router 3 re-approval") // Validate primary routes table state after router 3 re-approval - validatePrimaryRoutes(t, headscale, &routes.DebugRoutes{ + validatePrimaryRoutes(t, headscale, &types.DebugRoutes{ AvailableRoutes: map[types.NodeID][]netip.Prefix{ types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref}, types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},