From e99d8f3f880f627b7121ec8216cfde349ab6974a Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 31 Oct 2026 14:20:47 +0000 Subject: [PATCH] api/v2: list devices sorted by name SaaS sorts by name. NodeStore order moved on every write, so order-sensitive Terraform configs showed changes. --- hscontrol/api/v2/devices.go | 6 ++++++ hscontrol/apiv2_compat_test.go | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/hscontrol/api/v2/devices.go b/hscontrol/api/v2/devices.go index d8c6d39a4..eb723fd02 100644 --- a/hscontrol/api/v2/devices.go +++ b/hscontrol/api/v2/devices.go @@ -1,6 +1,7 @@ package apiv2 import ( + "cmp" "context" "net/http" "net/netip" @@ -175,6 +176,11 @@ func registerDevices(api huma.API, b Backend) { out.Body.Devices = append(out.Body.Devices, deviceFromView(node, allFields, b.Cfg.BaseDomain)) } + // The NodeStore holds no order, and Tailscale lists devices by name. + slices.SortFunc(out.Body.Devices, func(a, b Device) int { + return cmp.Compare(a.Name, b.Name) + }) + return out, nil }) diff --git a/hscontrol/apiv2_compat_test.go b/hscontrol/apiv2_compat_test.go index 42c5da41c..b6e8515e1 100644 --- a/hscontrol/apiv2_compat_test.go +++ b/hscontrol/apiv2_compat_test.go @@ -851,6 +851,16 @@ func (r *apiReplay) deviceListChecks(t *testing.T, a testcapture.Action, resp [] wantBy, gotBy := byLabel(want.Devices), byLabel(got.Devices) checks := []apiCheck{{"listed", sortedStrings(slices.Collect(maps.Keys(wantBy))), sortedStrings(slices.Collect(maps.Keys(gotBy)))}} + order := func(devs []apiBody) string { + labels := make([]string, 0, len(devs)) + for _, d := range devs { + labels = append(labels, r.label("device", d.ID)) + } + + return strings.Join(labels, ",") + } + checks = append(checks, apiCheck{"order", order(want.Devices), order(got.Devices)}) + for _, l := range slices.Sorted(maps.Keys(wantBy)) { g, ok := gotBy[l] if !ok {