all: adopt slices helpers

This commit is contained in:
Kristoffer Dalby 2026-06-16 06:57:11 +00:00
parent 60f0544b78
commit ac725f27e4
18 changed files with 94 additions and 166 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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
}

View file

@ -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].

View file

@ -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
}

View file

@ -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.

View file

@ -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 {

View file

@ -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)

View file

@ -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

View file

@ -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()
}

View file

@ -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

View file

@ -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
}
}
}
}
}

View file

@ -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

View file

@ -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) {

View file

@ -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.

View file

@ -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

View file

@ -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)
}

View file

@ -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