From ac725f27e4881a9c9df22fe8297ed27b748d3401 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 16 Jun 2026 06:57:11 +0000 Subject: [PATCH] all: adopt slices helpers --- cmd/headscale/cli/root.go | 11 +++--- cmd/headscale/cli/utils.go | 11 +++--- cmd/hi/stats.go | 17 ++++------ hscontrol/capver/capver.go | 8 ++--- hscontrol/db/node.go | 5 +-- hscontrol/db/sqliteconfig/config.go | 52 +++++++++++++++-------------- hscontrol/debug.go | 12 +++---- hscontrol/handlers.go | 16 +++------ hscontrol/mapper/builder.go | 6 ++-- hscontrol/mapper/node_conn.go | 41 +++++++++-------------- hscontrol/policy/policy.go | 11 +++--- hscontrol/policy/v2/policy.go | 15 ++------- hscontrol/policy/v2/sshtest.go | 15 ++------- hscontrol/realip.go | 11 +++--- hscontrol/types/change/change.go | 8 +---- hscontrol/types/node.go | 8 +---- hscontrol/util/prompt.go | 7 ++-- integration/hsic/hsic.go | 6 ++-- 18 files changed, 94 insertions(+), 166 deletions(-) diff --git a/cmd/headscale/cli/root.go b/cmd/headscale/cli/root.go index 212645cec..ff06ea24b 100644 --- a/cmd/headscale/cli/root.go +++ b/cmd/headscale/cli/root.go @@ -3,6 +3,7 @@ package cli import ( "os" "runtime" + "slices" "strings" "github.com/juanfont/headscale/hscontrol/types" @@ -95,13 +96,9 @@ func initConfig() { var prereleases = []string{"alpha", "beta", "rc", "dev"} func isPreReleaseVersion(version string) bool { - for _, unstable := range prereleases { - if strings.Contains(version, unstable) { - return true - } - } - - return false + return slices.ContainsFunc(prereleases, func(unstable string) bool { + return strings.Contains(version, unstable) + }) } // filterPreReleasesIfStable returns a function that filters out diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 53ae45138..1187de425 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "os" + "slices" "time" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" @@ -318,13 +319,9 @@ func printError(err error, outputFormat string) { } func hasMachineOutputFlag() bool { - for _, arg := range os.Args { - if arg == outputFormatJSON || arg == outputFormatJSONLine || arg == outputFormatYAML { - return true - } - } - - return false + return slices.ContainsFunc(os.Args, func(arg string) bool { + return arg == outputFormatJSON || arg == outputFormatJSONLine || arg == outputFormatYAML + }) } type tokenAuth struct { diff --git a/cmd/hi/stats.go b/cmd/hi/stats.go index 252b4eb2e..f56043d96 100644 --- a/cmd/hi/stats.go +++ b/cmd/hi/stats.go @@ -1,12 +1,13 @@ package main import ( + "cmp" "context" "encoding/json" "errors" "fmt" "log" - "sort" + "slices" "strings" "sync" "time" @@ -390,8 +391,8 @@ func (sc *StatsCollector) GetSummary() []ContainerStatsSummary { } // Sort by container name for consistent output - sort.Slice(summaries, func(i, j int) bool { - return summaries[i].ContainerName < summaries[j].ContainerName + slices.SortFunc(summaries, func(a, b ContainerStatsSummary) int { + return cmp.Compare(a.ContainerName, b.ContainerName) }) return summaries @@ -408,14 +409,8 @@ func calculateStatsSummary(values []float64) StatsSummary { sum := 0.0 for _, value := range values { - if value < minVal { - minVal = value - } - - if value > maxVal { - maxVal = value - } - + minVal = min(minVal, value) + maxVal = max(maxVal, value) sum += value } diff --git a/hscontrol/capver/capver.go b/hscontrol/capver/capver.go index 31b430447..6ff102b15 100644 --- a/hscontrol/capver/capver.go +++ b/hscontrol/capver/capver.go @@ -3,10 +3,11 @@ package capver //go:generate go run ../../tools/capver/main.go import ( + "maps" + "slices" "sort" "strings" - xmaps "golang.org/x/exp/maps" "tailscale.com/tailcfg" "tailscale.com/util/set" ) @@ -25,10 +26,7 @@ func CanOldCodeBeCleanedUp() { } func tailscaleVersSorted() []string { - vers := xmaps.Keys(tailscaleToCapVer) - sort.Strings(vers) - - return vers + return slices.Sorted(maps.Keys(tailscaleToCapVer)) } // TailscaleVersion returns the Tailscale version for the given [tailcfg.CapabilityVersion]. diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index 226f7b1d1..002bde590 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -1,10 +1,11 @@ package db import ( + "cmp" "errors" "fmt" "net/netip" - "sort" + "slices" "strconv" "sync" "testing" @@ -68,7 +69,7 @@ func ListPeers(tx *gorm.DB, nodeID types.NodeID, peerIDs ...types.NodeID) (types return types.Nodes{}, err } - sort.Slice(nodes, func(i, j int) bool { return nodes[i].ID < nodes[j].ID }) + slices.SortFunc(nodes, func(a, b *types.Node) int { return cmp.Compare(a.ID, b.ID) }) return nodes, nil } diff --git a/hscontrol/db/sqliteconfig/config.go b/hscontrol/db/sqliteconfig/config.go index 66fec5906..9d818ae1b 100644 --- a/hscontrol/db/sqliteconfig/config.go +++ b/hscontrol/db/sqliteconfig/config.go @@ -5,6 +5,7 @@ package sqliteconfig import ( "errors" "fmt" + "slices" "strings" ) @@ -90,15 +91,15 @@ const ( JournalModeOff JournalMode = "OFF" ) +// validJournalModes lists the accepted JournalMode values. +var validJournalModes = []JournalMode{ + JournalModeWAL, JournalModeDelete, JournalModeTruncate, + JournalModePersist, JournalModeMemory, JournalModeOff, +} + // IsValid returns true if the JournalMode is valid. func (j JournalMode) IsValid() bool { - switch j { - case JournalModeWAL, JournalModeDelete, JournalModeTruncate, - JournalModePersist, JournalModeMemory, JournalModeOff: - return true - default: - return false - } + return slices.Contains(validJournalModes, j) } // AutoVacuum represents SQLite auto_vacuum pragma values. @@ -142,14 +143,14 @@ const ( AutoVacuumIncremental AutoVacuum = "INCREMENTAL" ) +// validAutoVacuums lists the accepted AutoVacuum values. +var validAutoVacuums = []AutoVacuum{ + AutoVacuumNone, AutoVacuumFull, AutoVacuumIncremental, +} + // IsValid returns true if the AutoVacuum is valid. func (a AutoVacuum) IsValid() bool { - switch a { - case AutoVacuumNone, AutoVacuumFull, AutoVacuumIncremental: - return true - default: - return false - } + return slices.Contains(validAutoVacuums, a) } // Synchronous represents SQLite synchronous pragma values. @@ -201,14 +202,14 @@ const ( SynchronousExtra Synchronous = "EXTRA" ) +// validSynchronous lists the accepted Synchronous values. +var validSynchronous = []Synchronous{ + SynchronousOff, SynchronousNormal, SynchronousFull, SynchronousExtra, +} + // IsValid returns true if the Synchronous is valid. func (s Synchronous) IsValid() bool { - switch s { - case SynchronousOff, SynchronousNormal, SynchronousFull, SynchronousExtra: - return true - default: - return false - } + return slices.Contains(validSynchronous, s) } // TxLock represents SQLite transaction lock mode. @@ -252,14 +253,15 @@ const ( TxLockExclusive TxLock = "exclusive" ) +// validTxLocks lists the accepted TxLock values; the empty string is valid +// and selects the driver default. +var validTxLocks = []TxLock{ + TxLockDeferred, TxLockImmediate, TxLockExclusive, "", +} + // IsValid returns true if the TxLock is valid. func (t TxLock) IsValid() bool { - switch t { - case TxLockDeferred, TxLockImmediate, TxLockExclusive, "": - return true - default: - return false - } + return slices.Contains(validTxLocks, t) } // Config holds SQLite database configuration with type-safe enums. diff --git a/hscontrol/debug.go b/hscontrol/debug.go index c9e44637c..cc2a360bf 100644 --- a/hscontrol/debug.go +++ b/hscontrol/debug.go @@ -1,12 +1,14 @@ package hscontrol import ( + "cmp" "context" "encoding/json" "fmt" "net" "net/http" "net/netip" + "slices" "strings" "time" @@ -262,13 +264,9 @@ func (h *Headscale) debugBatcher() string { } // Sort by node ID - for i := 0; i < len(nodes); i++ { - for j := i + 1; j < len(nodes); j++ { - if nodes[i].id > nodes[j].id { - nodes[i], nodes[j] = nodes[j], nodes[i] - } - } - } + slices.SortFunc(nodes, func(a, b nodeStatus) int { + return cmp.Compare(a.id, b.id) + }) // Output sorted nodes for _, node := range nodes { diff --git a/hscontrol/handlers.go b/hscontrol/handlers.go index 3914c6dcc..ccae09834 100644 --- a/hscontrol/handlers.go +++ b/hscontrol/handlers.go @@ -148,20 +148,12 @@ func (h *Headscale) handleVerifyRequest( return NewHTTPError(http.StatusBadRequest, "Bad Request: invalid JSON", fmt.Errorf("parsing DERP client request: %w", err)) } - nodes := h.state.ListNodes() - - // Check if any node has the requested NodeKey - var nodeKeyFound bool - - for _, node := range nodes.All() { - if node.NodeKey() == derpAdmitClientRequest.NodePublic { - nodeKeyFound = true - break - } - } + allow := h.state.ListNodes().ContainsFunc(func(n types.NodeView) bool { + return n.NodeKey() == derpAdmitClientRequest.NodePublic + }) resp := &tailcfg.DERPAdmitClientResponse{ - Allow: nodeKeyFound, + Allow: allow, } return json.NewEncoder(writer).Encode(resp) diff --git a/hscontrol/mapper/builder.go b/hscontrol/mapper/builder.go index ba773e6b0..4a8e25aed 100644 --- a/hscontrol/mapper/builder.go +++ b/hscontrol/mapper/builder.go @@ -1,9 +1,9 @@ package mapper import ( + "cmp" "net/netip" "slices" - "sort" "time" "github.com/juanfont/headscale/hscontrol/policy" @@ -301,8 +301,8 @@ func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.NodeView]) ( } // Peers is always returned sorted by Node.ID. - sort.SliceStable(tailPeers, func(x, y int) bool { - return tailPeers[x].ID < tailPeers[y].ID + slices.SortStableFunc(tailPeers, func(a, b *tailcfg.Node) int { + return cmp.Compare(a.ID, b.ID) }) return tailPeers, nil diff --git a/hscontrol/mapper/node_conn.go b/hscontrol/mapper/node_conn.go index 18ab34f41..27a43a13b 100644 --- a/hscontrol/mapper/node_conn.go +++ b/hscontrol/mapper/node_conn.go @@ -3,6 +3,7 @@ package mapper import ( "errors" "fmt" + "slices" "strconv" "sync" "sync/atomic" @@ -142,9 +143,7 @@ func (mc *multiChannelNodeConn) stopConnection(conn *connectionEntry) { // Caller must hold mc.mutex. func (mc *multiChannelNodeConn) removeConnectionAtIndexLocked(i int, stopConnection bool) *connectionEntry { conn := mc.connections[i] - copy(mc.connections[i:], mc.connections[i+1:]) - mc.connections[len(mc.connections)-1] = nil // release pointer for GC - mc.connections = mc.connections[:len(mc.connections)-1] + mc.connections = slices.Delete(mc.connections, i, i+1) if stopConnection { mc.stopConnection(conn) @@ -353,32 +352,22 @@ func (mc *multiChannelNodeConn) send(data *tailcfg.MapResponse) error { // Remove by pointer identity: only remove entries that still exist // in the current connections slice and match a failed pointer. // New connections added since the snapshot are not affected. - failedSet := make(map[*connectionEntry]struct{}, len(failed)) - for _, f := range failed { - failedSet[f] = struct{}{} - } - - clean := mc.connections[:0] - for _, conn := range mc.connections { - if _, isFailed := failedSet[conn]; !isFailed { - clean = append(clean, conn) - } else { - mc.log.Debug(). - Str(zf.ConnID, conn.id). - Msg("send: removing failed connection") - // Tear down the owning session so the old serveLongPoll - // goroutine exits instead of lingering as a stale session. - mc.stopConnection(conn) + // DeleteFunc preserves order and zeroes trailing slots so removed + // *connectionEntry values are not retained by the backing array. + mc.connections = slices.DeleteFunc(mc.connections, func(conn *connectionEntry) bool { + if !slices.Contains(failed, conn) { + return false } - } - // Nil out trailing slots so removed *connectionEntry values - // are not retained by the backing array. - for i := len(clean); i < len(mc.connections); i++ { - mc.connections[i] = nil - } + mc.log.Debug(). + Str(zf.ConnID, conn.id). + Msg("send: removing failed connection") + // Tear down the owning session so the old serveLongPoll + // goroutine exits instead of lingering as a stale session. + mc.stopConnection(conn) - mc.connections = clean + return true + }) mc.mutex.Unlock() } diff --git a/hscontrol/policy/policy.go b/hscontrol/policy/policy.go index 82e942f9d..94316eb72 100644 --- a/hscontrol/policy/policy.go +++ b/hscontrol/policy/policy.go @@ -8,7 +8,6 @@ import ( "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" "github.com/rs/zerolog/log" - "github.com/samber/lo" "tailscale.com/types/views" ) @@ -66,8 +65,7 @@ func ApproveRoutesWithPolicy(pm PolicyManager, nv types.NodeView, currentApprove } // Start with ALL currently approved routes - we never remove approved routes - newApproved := make([]netip.Prefix, len(currentApproved)) - copy(newApproved, currentApproved) + newApproved := slices.Clone(currentApproved) // Then, check for new routes that can be auto-approved for _, route := range announcedRoutes { @@ -86,13 +84,12 @@ func ApproveRoutesWithPolicy(pm PolicyManager, nv types.NodeView, currentApprove // Sort and deduplicate slices.SortFunc(newApproved, netip.Prefix.Compare) newApproved = slices.Compact(newApproved) - newApproved = lo.Filter(newApproved, func(route netip.Prefix, index int) bool { - return route.IsValid() + newApproved = slices.DeleteFunc(newApproved, func(route netip.Prefix) bool { + return !route.IsValid() }) // Sort the current approved for comparison - sortedCurrent := make([]netip.Prefix, len(currentApproved)) - copy(sortedCurrent, currentApproved) + sortedCurrent := slices.Clone(currentApproved) slices.SortFunc(sortedCurrent, netip.Prefix.Compare) // Only update if the routes actually changed diff --git a/hscontrol/policy/v2/policy.go b/hscontrol/policy/v2/policy.go index e25166789..49750713c 100644 --- a/hscontrol/policy/v2/policy.go +++ b/hscontrol/policy/v2/policy.go @@ -1455,20 +1455,9 @@ func (pm *PolicyManager) invalidateAutogroupSelfCache(oldNodes, newNodes views.S affectedUsers[newNode.TypedUserID()] = struct{}{} } - // Check if IPs changed (simple check - could be more sophisticated) - oldIPs := oldNode.IPs() - - newIPs := newNode.IPs() - if len(oldIPs) != len(newIPs) { + // Check if IPs changed. + if !slices.Equal(oldNode.IPs(), newNode.IPs()) { affectedUsers[newNode.TypedUserID()] = struct{}{} - } else { - // Check if any IPs are different - for i, oldIP := range oldIPs { - if i >= len(newIPs) || oldIP != newIPs[i] { - affectedUsers[newNode.TypedUserID()] = struct{}{} - break - } - } } } } diff --git a/hscontrol/policy/v2/sshtest.go b/hscontrol/policy/v2/sshtest.go index 968eb6a25..ec0abb03b 100644 --- a/hscontrol/policy/v2/sshtest.go +++ b/hscontrol/policy/v2/sshtest.go @@ -2,6 +2,7 @@ package v2 import ( "fmt" + "maps" "net/netip" "slices" "strings" @@ -92,14 +93,7 @@ func (r SSHPolicyTestResults) Errors() string { } func sortedUsers(m map[string][]string) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - - slices.Sort(keys) - - return keys + return slices.Sorted(maps.Keys(m)) } // displayUser shows an empty username as `""` rather than blank. @@ -394,10 +388,7 @@ func resolveSSHTestSource( return nil, 0, nil } - out := make([]netip.Addr, 0) - for a := range addrs.Iter() { - out = append(out, a) - } + out := slices.Collect(addrs.Iter()) var userID uint diff --git a/hscontrol/realip.go b/hscontrol/realip.go index 6cfd1a1a0..af41aa7bd 100644 --- a/hscontrol/realip.go +++ b/hscontrol/realip.go @@ -5,6 +5,7 @@ import ( "net" "net/http" "net/netip" + "slices" realclientip "github.com/realclientip/realclientip-go" ) @@ -77,13 +78,9 @@ func peerTrusted(remoteAddr string, trusted []netip.Prefix) bool { return false } - for _, p := range trusted { - if p.Contains(addr) { - return true - } - } - - return false + return slices.ContainsFunc(trusted, func(p netip.Prefix) bool { + return p.Contains(addr) + }) } func parsePeerAddr(remoteAddr string) (netip.Addr, bool) { diff --git a/hscontrol/types/change/change.go b/hscontrol/types/change/change.go index b6015b73e..58d494da8 100644 --- a/hscontrol/types/change/change.go +++ b/hscontrol/types/change/change.go @@ -213,13 +213,7 @@ func (r Change) ShouldSendToNode(nodeID types.NodeID) bool { // HasFull returns true if any response in the slice is a full update ([Change.IsFull]). func HasFull(rs []Change) bool { - for _, r := range rs { - if r.IsFull() { - return true - } - } - - return false + return slices.ContainsFunc(rs, Change.IsFull) } // SplitTargetedAndBroadcast separates responses into targeted (to specific node) and broadcast. diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 6212c33ea..b487c5813 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -243,13 +243,7 @@ func (node *Node) IPs() []netip.Addr { // HasIP reports if a node has a given IP address. func (node *Node) HasIP(i netip.Addr) bool { - for _, ip := range node.IPs() { - if ip.Compare(i) == 0 { - return true - } - } - - return false + return slices.Contains(node.IPs(), i) } // IsTagged reports if a device is tagged and therefore should not be treated diff --git a/hscontrol/util/prompt.go b/hscontrol/util/prompt.go index 5f0adedef..8abda2305 100644 --- a/hscontrol/util/prompt.go +++ b/hscontrol/util/prompt.go @@ -3,6 +3,7 @@ package util import ( "fmt" "os" + "slices" "strings" ) @@ -18,10 +19,6 @@ func YesNo(msg string) bool { _, _ = fmt.Scanln(&resp) resp = strings.ToLower(resp) - switch resp { - case "y", "yes", "sure": - return true - } - return false + return slices.Contains([]string{"y", "yes", "sure"}, resp) } diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index dd887be8a..337c4d850 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -18,7 +18,7 @@ import ( "os" "path" "path/filepath" - "sort" + "slices" "strconv" "strings" "time" @@ -1295,8 +1295,8 @@ func (t *HeadscaleInContainer) ListNodes( } } - sort.Slice(ret, func(i, j int) bool { - return cmp.Compare(ret[i].GetId(), ret[j].GetId()) == -1 + slices.SortFunc(ret, func(a, b *v1.Node) int { + return cmp.Compare(a.GetId(), b.GetId()) }) return ret, nil