all: modernize sorting with slices package

Replace deprecated sort package functions with their modern
slices package equivalents:

- sort.Slice -> slices.SortFunc
- sort.SliceStable -> slices.SortStableFunc
- sort.Sort -> slices.Sort
- sort.Strings -> slices.Sort

Also removes the now-unused sort.Interface implementation
(Len, Less, Swap methods) from types.NodeIDs since slices.Sort
works directly with ordered types.
This commit is contained in:
Kristoffer Dalby 2026-01-20 14:22:46 +00:00
parent f9b3265158
commit 094faf7a6a
12 changed files with 98 additions and 111 deletions

View file

@ -1,12 +1,13 @@
package main
import (
"cmp"
"context"
"encoding/json"
"errors"
"fmt"
"log"
"sort"
"slices"
"strings"
"sync"
"time"
@ -371,8 +372,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

View file

@ -1,13 +1,13 @@
package db
import (
"cmp"
"encoding/json"
"errors"
"fmt"
"net/netip"
"regexp"
"slices"
"sort"
"strconv"
"strings"
"sync"
@ -20,7 +20,6 @@ import (
"gorm.io/gorm"
"tailscale.com/net/tsaddr"
"tailscale.com/types/key"
"tailscale.com/types/ptr"
)
const (
@ -60,7 +59,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
}
@ -668,7 +667,7 @@ func (hsdb *HSDatabase) CreateNodeForTest(user *types.User, hostname ...string)
Hostname: nodeName,
UserID: &user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: ptr.To(pak.ID),
AuthKeyID: new(pak.ID),
}
err = hsdb.DB.Save(node).Error

View file

@ -11,7 +11,6 @@ import (
"github.com/juanfont/headscale/hscontrol/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tailscale.com/types/ptr"
)
func TestCreatePreAuthKey(t *testing.T) {
@ -24,7 +23,7 @@ func TestCreatePreAuthKey(t *testing.T) {
test: func(t *testing.T, db *HSDatabase) {
t.Helper()
_, err := db.CreatePreAuthKey(ptr.To(types.UserID(12345)), true, false, nil, nil)
_, err := db.CreatePreAuthKey(new(types.UserID(12345)), true, false, nil, nil)
assert.Error(t, err)
},
},
@ -127,7 +126,7 @@ func TestCannotDeleteAssignedPreAuthKey(t *testing.T) {
Hostname: "testest",
UserID: &user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: ptr.To(key.ID),
AuthKeyID: new(key.ID),
}
db.DB.Save(&node)

View file

@ -1,9 +1,10 @@
package mapper
import (
"cmp"
"errors"
"net/netip"
"sort"
"slices"
"time"
"github.com/juanfont/headscale/hscontrol/policy"
@ -261,8 +262,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

@ -956,14 +956,7 @@ func (pm *PolicyManager) invalidateGlobalPolicyCache(newNodes views.Slice[types.
// It will return a Owners list where all the Tag types have been resolved to their underlying Owners.
func flattenTags(tagOwners TagOwners, tag Tag, visiting map[Tag]bool, chain []Tag) (Owners, error) {
if visiting[tag] {
cycleStart := 0
for i, t := range chain {
if t == tag {
cycleStart = i
break
}
}
cycleStart := slices.Index(chain, tag)
cycleTags := make([]string, len(chain[cycleStart:]))
for i, t := range chain[cycleStart:] {

View file

@ -333,7 +333,7 @@ func NodeOnline(nodeID types.NodeID) Change {
PeerPatches: []*tailcfg.PeerChange{
{
NodeID: nodeID.NodeID(),
Online: ptrTo(true),
Online: new(true),
},
},
}
@ -346,7 +346,7 @@ func NodeOffline(nodeID types.NodeID) Change {
PeerPatches: []*tailcfg.PeerChange{
{
NodeID: nodeID.NodeID(),
Online: ptrTo(false),
Online: new(false),
},
},
}
@ -366,8 +366,10 @@ func KeyExpiry(nodeID types.NodeID, expiry *time.Time) Change {
}
// ptrTo returns a pointer to the given value.
//
//go:fix inline
func ptrTo[T any](v T) *T {
return &v
return new(v)
}
// High-level change constructors

View file

@ -16,8 +16,8 @@ func TestChange_FieldSync(t *testing.T) {
typ := reflect.TypeFor[Change]()
boolCount := 0
for i := range typ.NumField() {
if typ.Field(i).Type.Kind() == reflect.Bool {
for field := range typ.Fields() {
if field.Type.Kind() == reflect.Bool {
boolCount++
}
}

View file

@ -1,15 +1,16 @@
package integration
import (
"cmp"
"maps"
"net/netip"
"net/url"
"sort"
"slices"
"strconv"
"testing"
"time"
"github.com/google/go-cmp/cmp"
gocmp "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
@ -111,11 +112,11 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
},
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
slices.SortFunc(listUsers, func(a, b *v1.User) int {
return cmp.Compare(a.GetId(), b.GetId())
})
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := gocmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
t.Fatalf("unexpected users: %s", diff)
}
}
@ -388,11 +389,11 @@ func TestOIDC024UserCreation(t *testing.T) {
listUsers, err := headscale.ListUsers()
require.NoError(t, err)
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
slices.SortFunc(listUsers, func(a, b *v1.User) int {
return cmp.Compare(a.GetId(), b.GetId())
})
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := gocmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
t.Errorf("unexpected users: %s", diff)
}
})
@ -517,11 +518,11 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
},
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
slices.SortFunc(listUsers, func(a, b *v1.User) int {
return cmp.Compare(a.GetId(), b.GetId())
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := gocmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
ct.Errorf("User validation failed after first login - unexpected users: %s", diff)
}
}, 30*time.Second, 1*time.Second, "validating user1 creation after initial OIDC login")
@ -599,11 +600,11 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
},
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
slices.SortFunc(listUsers, func(a, b *v1.User) int {
return cmp.Compare(a.GetId(), b.GetId())
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := gocmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
ct.Errorf("User validation failed after user2 login - expected both user1 and user2: %s", diff)
}
}, 30*time.Second, 1*time.Second, "validating both user1 and user2 exist after second OIDC login")
@ -763,11 +764,11 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
},
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
slices.SortFunc(listUsers, func(a, b *v1.User) int {
return cmp.Compare(a.GetId(), b.GetId())
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := gocmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
ct.Errorf("Final user validation failed - both users should persist after relogin cycle: %s", diff)
}
}, 30*time.Second, 1*time.Second, "validating user persistence after complete relogin cycle (user1->user2->user1)")
@ -935,13 +936,11 @@ func TestOIDCFollowUpUrl(t *testing.T) {
},
}
sort.Slice(
listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
},
)
slices.SortFunc(listUsers, func(a, b *v1.User) int {
return cmp.Compare(a.GetId(), b.GetId())
})
if diff := cmp.Diff(
if diff := gocmp.Diff(
wantUsers,
listUsers,
cmpopts.IgnoreUnexported(v1.User{}),
@ -1046,13 +1045,11 @@ func TestOIDCMultipleOpenedLoginUrls(t *testing.T) {
},
}
sort.Slice(
listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
},
)
slices.SortFunc(listUsers, func(a, b *v1.User) int {
return cmp.Compare(a.GetId(), b.GetId())
})
if diff := cmp.Diff(
if diff := gocmp.Diff(
wantUsers,
listUsers,
cmpopts.IgnoreUnexported(v1.User{}),
@ -1155,11 +1152,11 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
},
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
slices.SortFunc(listUsers, func(a, b *v1.User) int {
return cmp.Compare(a.GetId(), b.GetId())
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := gocmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
ct.Errorf("User validation failed after first login - unexpected users: %s", diff)
}
}, 30*time.Second, 1*time.Second, "validating user1 creation after initial OIDC login")
@ -1249,11 +1246,11 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
},
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
slices.SortFunc(listUsers, func(a, b *v1.User) int {
return cmp.Compare(a.GetId(), b.GetId())
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := gocmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
ct.Errorf("Final user validation failed - user1 should persist after same-user relogin: %s", diff)
}
}, 30*time.Second, 1*time.Second, "validating user1 persistence after same-user OIDC relogin cycle")

View file

@ -26,7 +26,6 @@ import (
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"tailscale.com/tailcfg"
"tailscale.com/types/ptr"
)
const (
@ -839,32 +838,32 @@ func wildcard() policyv2.Alias {
// usernamep returns a pointer to a Username as an Alias for policy v2 configurations.
// Used in ACL rules to reference specific users in network access policies.
func usernamep(name string) policyv2.Alias {
return ptr.To(policyv2.Username(name))
return new(policyv2.Username(name))
}
// hostp returns a pointer to a Host as an Alias for policy v2 configurations.
// Used in ACL rules to reference specific hosts in network access policies.
func hostp(name string) policyv2.Alias {
return ptr.To(policyv2.Host(name))
return new(policyv2.Host(name))
}
// groupp returns a pointer to a Group as an Alias for policy v2 configurations.
// Used in ACL rules to reference user groups in network access policies.
func groupp(name string) policyv2.Alias {
return ptr.To(policyv2.Group(name))
return new(policyv2.Group(name))
}
// tagp returns a pointer to a Tag as an Alias for policy v2 configurations.
// Used in ACL rules to reference node tags in network access policies.
func tagp(name string) policyv2.Alias {
return ptr.To(policyv2.Tag(name))
return new(policyv2.Tag(name))
}
// prefixp returns a pointer to a Prefix from a CIDR string for policy v2 configurations.
// Converts CIDR notation to policy prefix format for network range specifications.
func prefixp(cidr string) policyv2.Alias {
prefix := netip.MustParsePrefix(cidr)
return ptr.To(policyv2.Prefix(prefix))
return new(policyv2.Prefix(prefix))
}
// aliasWithPorts creates an AliasWithPorts structure from an alias and port ranges.
@ -880,31 +879,31 @@ func aliasWithPorts(alias policyv2.Alias, ports ...tailcfg.PortRange) policyv2.A
// usernameOwner returns a Username as an Owner for use in TagOwners policies.
// Specifies which users can assign and manage specific tags in ACL configurations.
func usernameOwner(name string) policyv2.Owner {
return ptr.To(policyv2.Username(name))
return new(policyv2.Username(name))
}
// groupOwner returns a Group as an Owner for use in TagOwners policies.
// Specifies which groups can assign and manage specific tags in ACL configurations.
func groupOwner(name string) policyv2.Owner {
return ptr.To(policyv2.Group(name))
return new(policyv2.Group(name))
}
// usernameApprover returns a Username as an AutoApprover for subnet route policies.
// Specifies which users can automatically approve subnet route advertisements.
func usernameApprover(name string) policyv2.AutoApprover {
return ptr.To(policyv2.Username(name))
return new(policyv2.Username(name))
}
// groupApprover returns a Group as an AutoApprover for subnet route policies.
// Specifies which groups can automatically approve subnet route advertisements.
func groupApprover(name string) policyv2.AutoApprover {
return ptr.To(policyv2.Group(name))
return new(policyv2.Group(name))
}
// tagApprover returns a Tag as an AutoApprover for subnet route policies.
// Specifies which tagged nodes can automatically approve subnet route advertisements.
func tagApprover(name string) policyv2.AutoApprover {
return ptr.To(policyv2.Tag(name))
return new(policyv2.Tag(name))
}
// oidcMockUser creates a MockUser for OIDC authentication testing.

View file

@ -16,7 +16,7 @@ import (
"os"
"path"
"path/filepath"
"sort"
"slices"
"strconv"
"strings"
"time"
@ -1232,8 +1232,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

View file

@ -7,7 +7,6 @@ import (
"maps"
"net/netip"
"slices"
"sort"
"strconv"
"strings"
"testing"
@ -287,11 +286,10 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf("webservice: %s, %s", webip.String(), weburl)
// Sort nodes by ID
sort.SliceStable(allClients, func(i, j int) bool {
statusI := allClients[i].MustStatus()
statusJ := allClients[j].MustStatus()
return statusI.Self.ID < statusJ.Self.ID
slices.SortStableFunc(allClients, func(a, b TailscaleClient) int {
statusA := a.MustStatus()
statusB := b.MustStatus()
return cmp.Compare(statusA.Self.ID, statusB.Self.ID)
})
// This is ok because the scenario makes users in order, so the three first
@ -1359,10 +1357,10 @@ func TestSubnetRouteACL(t *testing.T) {
}
// Sort nodes by ID
sort.SliceStable(allClients, func(i, j int) bool {
statusI := allClients[i].MustStatus()
statusJ := allClients[j].MustStatus()
return statusI.Self.ID < statusJ.Self.ID
slices.SortStableFunc(allClients, func(a, b TailscaleClient) int {
statusA := a.MustStatus()
statusB := b.MustStatus()
return cmp.Compare(statusA.Self.ID, statusB.Self.ID)
})
subRouter1 := allClients[0]
@ -2403,11 +2401,10 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
t.Logf("webservice: %s, %s", webip.String(), weburl)
// Sort nodes by ID
sort.SliceStable(allClients, func(i, j int) bool {
statusI := allClients[i].MustStatus()
statusJ := allClients[j].MustStatus()
return statusI.Self.ID < statusJ.Self.ID
slices.SortStableFunc(allClients, func(a, b TailscaleClient) int {
statusA := a.MustStatus()
statusB := b.MustStatus()
return cmp.Compare(statusA.Self.ID, statusB.Self.ID)
})
// This is ok because the scenario makes users in order, so the three first

View file

@ -1,7 +1,7 @@
package integration
import (
"sort"
"slices"
"testing"
"time"
@ -13,7 +13,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tailscale.com/tailcfg"
"tailscale.com/types/ptr"
)
const tagTestUser = "taguser"
@ -30,9 +29,9 @@ const tagTestUser = "taguser"
func tagsTestPolicy() *policyv2.Policy {
return &policyv2.Policy{
TagOwners: policyv2.TagOwners{
"tag:valid-owned": policyv2.Owners{ptr.To(policyv2.Username(tagTestUser + "@"))},
"tag:second": policyv2.Owners{ptr.To(policyv2.Username(tagTestUser + "@"))},
"tag:valid-unowned": policyv2.Owners{ptr.To(policyv2.Username("other-user@"))},
"tag:valid-owned": policyv2.Owners{new(policyv2.Username(tagTestUser + "@"))},
"tag:second": policyv2.Owners{new(policyv2.Username(tagTestUser + "@"))},
"tag:valid-unowned": policyv2.Owners{new(policyv2.Username("other-user@"))},
// Note: tag:nonexistent deliberately NOT defined
},
ACLs: []policyv2.ACL{
@ -51,11 +50,11 @@ func tagsEqual(actual, expected []string) bool {
return false
}
sortedActual := append([]string{}, actual...)
sortedExpected := append([]string{}, expected...)
sortedActual := slices.Clone(actual)
sortedExpected := slices.Clone(expected)
sort.Strings(sortedActual)
sort.Strings(sortedExpected)
slices.Sort(sortedActual)
slices.Sort(sortedExpected)
for i := range sortedActual {
if sortedActual[i] != sortedExpected[i] {
@ -69,11 +68,11 @@ func tagsEqual(actual, expected []string) bool {
// assertNodeHasTagsWithCollect asserts that a node has exactly the expected tags (order-independent).
func assertNodeHasTagsWithCollect(c *assert.CollectT, node *v1.Node, expectedTags []string) {
actualTags := node.GetTags()
sortedActual := append([]string{}, actualTags...)
sortedExpected := append([]string{}, expectedTags...)
sortedActual := slices.Clone(actualTags)
sortedExpected := slices.Clone(expectedTags)
sort.Strings(sortedActual)
sort.Strings(sortedExpected)
slices.Sort(sortedActual)
slices.Sort(sortedExpected)
assert.Equal(c, sortedExpected, sortedActual, "Node %s tags mismatch", node.GetName())
}
@ -102,11 +101,11 @@ func assertNodeSelfHasTagsWithCollect(c *assert.CollectT, client TailscaleClient
}
}
sortedActual := append([]string{}, actualTagsSlice...)
sortedExpected := append([]string{}, expectedTags...)
sortedActual := slices.Clone(actualTagsSlice)
sortedExpected := slices.Clone(expectedTags)
sort.Strings(sortedActual)
sort.Strings(sortedExpected)
slices.Sort(sortedActual)
slices.Sort(sortedExpected)
assert.Equal(c, sortedExpected, sortedActual, "Client %s self tags mismatch", client.Hostname())
}
@ -2507,11 +2506,11 @@ func assertNetmapSelfHasTagsWithCollect(c *assert.CollectT, client TailscaleClie
}
}
sortedActual := append([]string{}, actualTagsSlice...)
sortedExpected := append([]string{}, expectedTags...)
sortedActual := slices.Clone(actualTagsSlice)
sortedExpected := slices.Clone(expectedTags)
sort.Strings(sortedActual)
sort.Strings(sortedExpected)
slices.Sort(sortedActual)
slices.Sort(sortedExpected)
assert.Equal(c, sortedExpected, sortedActual, "Client %s netmap self tags mismatch", client.Hostname())
}