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 package main
import ( import (
"cmp"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"log" "log"
"sort" "slices"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -371,8 +372,8 @@ func (sc *StatsCollector) GetSummary() []ContainerStatsSummary {
} }
// Sort by container name for consistent output // Sort by container name for consistent output
sort.Slice(summaries, func(i, j int) bool { slices.SortFunc(summaries, func(a, b ContainerStatsSummary) int {
return summaries[i].ContainerName < summaries[j].ContainerName return cmp.Compare(a.ContainerName, b.ContainerName)
}) })
return summaries return summaries

View file

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

View file

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

View file

@ -1,9 +1,10 @@
package mapper package mapper
import ( import (
"cmp"
"errors" "errors"
"net/netip" "net/netip"
"sort" "slices"
"time" "time"
"github.com/juanfont/headscale/hscontrol/policy" "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. // Peers is always returned sorted by Node.ID.
sort.SliceStable(tailPeers, func(x, y int) bool { slices.SortStableFunc(tailPeers, func(a, b *tailcfg.Node) int {
return tailPeers[x].ID < tailPeers[y].ID return cmp.Compare(a.ID, b.ID)
}) })
return tailPeers, nil 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. // 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) { func flattenTags(tagOwners TagOwners, tag Tag, visiting map[Tag]bool, chain []Tag) (Owners, error) {
if visiting[tag] { if visiting[tag] {
cycleStart := 0 cycleStart := slices.Index(chain, tag)
for i, t := range chain {
if t == tag {
cycleStart = i
break
}
}
cycleTags := make([]string, len(chain[cycleStart:])) cycleTags := make([]string, len(chain[cycleStart:]))
for i, t := range chain[cycleStart:] { for i, t := range chain[cycleStart:] {

View file

@ -333,7 +333,7 @@ func NodeOnline(nodeID types.NodeID) Change {
PeerPatches: []*tailcfg.PeerChange{ PeerPatches: []*tailcfg.PeerChange{
{ {
NodeID: nodeID.NodeID(), NodeID: nodeID.NodeID(),
Online: ptrTo(true), Online: new(true),
}, },
}, },
} }
@ -346,7 +346,7 @@ func NodeOffline(nodeID types.NodeID) Change {
PeerPatches: []*tailcfg.PeerChange{ PeerPatches: []*tailcfg.PeerChange{
{ {
NodeID: nodeID.NodeID(), 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. // ptrTo returns a pointer to the given value.
//
//go:fix inline
func ptrTo[T any](v T) *T { func ptrTo[T any](v T) *T {
return &v return new(v)
} }
// High-level change constructors // High-level change constructors

View file

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

View file

@ -1,15 +1,16 @@
package integration package integration
import ( import (
"cmp"
"maps" "maps"
"net/netip" "net/netip"
"net/url" "net/url"
"sort" "slices"
"strconv" "strconv"
"testing" "testing"
"time" "time"
"github.com/google/go-cmp/cmp" gocmp "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2" 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 { slices.SortFunc(listUsers, func(a, b *v1.User) int {
return listUsers[i].GetId() < listUsers[j].GetId() 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) t.Fatalf("unexpected users: %s", diff)
} }
} }
@ -388,11 +389,11 @@ func TestOIDC024UserCreation(t *testing.T) {
listUsers, err := headscale.ListUsers() listUsers, err := headscale.ListUsers()
require.NoError(t, err) require.NoError(t, err)
sort.Slice(listUsers, func(i, j int) bool { slices.SortFunc(listUsers, func(a, b *v1.User) int {
return listUsers[i].GetId() < listUsers[j].GetId() 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) t.Errorf("unexpected users: %s", diff)
} }
}) })
@ -517,11 +518,11 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
}, },
} }
sort.Slice(listUsers, func(i, j int) bool { slices.SortFunc(listUsers, func(a, b *v1.User) int {
return listUsers[i].GetId() < listUsers[j].GetId() 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) 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") }, 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 { slices.SortFunc(listUsers, func(a, b *v1.User) int {
return listUsers[i].GetId() < listUsers[j].GetId() 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) 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") }, 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 { slices.SortFunc(listUsers, func(a, b *v1.User) int {
return listUsers[i].GetId() < listUsers[j].GetId() 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) 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)") }, 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( slices.SortFunc(listUsers, func(a, b *v1.User) int {
listUsers, func(i, j int) bool { return cmp.Compare(a.GetId(), b.GetId())
return listUsers[i].GetId() < listUsers[j].GetId() })
},
)
if diff := cmp.Diff( if diff := gocmp.Diff(
wantUsers, wantUsers,
listUsers, listUsers,
cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreUnexported(v1.User{}),
@ -1046,13 +1045,11 @@ func TestOIDCMultipleOpenedLoginUrls(t *testing.T) {
}, },
} }
sort.Slice( slices.SortFunc(listUsers, func(a, b *v1.User) int {
listUsers, func(i, j int) bool { return cmp.Compare(a.GetId(), b.GetId())
return listUsers[i].GetId() < listUsers[j].GetId() })
},
)
if diff := cmp.Diff( if diff := gocmp.Diff(
wantUsers, wantUsers,
listUsers, listUsers,
cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreUnexported(v1.User{}),
@ -1155,11 +1152,11 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
}, },
} }
sort.Slice(listUsers, func(i, j int) bool { slices.SortFunc(listUsers, func(a, b *v1.User) int {
return listUsers[i].GetId() < listUsers[j].GetId() 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) 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") }, 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 { slices.SortFunc(listUsers, func(a, b *v1.User) int {
return listUsers[i].GetId() < listUsers[j].GetId() 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) 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") }, 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/maps"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/ptr"
) )
const ( const (
@ -839,32 +838,32 @@ func wildcard() policyv2.Alias {
// usernamep returns a pointer to a Username as an Alias for policy v2 configurations. // 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. // Used in ACL rules to reference specific users in network access policies.
func usernamep(name string) policyv2.Alias { 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. // 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. // Used in ACL rules to reference specific hosts in network access policies.
func hostp(name string) policyv2.Alias { 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. // 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. // Used in ACL rules to reference user groups in network access policies.
func groupp(name string) policyv2.Alias { 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. // 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. // Used in ACL rules to reference node tags in network access policies.
func tagp(name string) policyv2.Alias { 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. // 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. // Converts CIDR notation to policy prefix format for network range specifications.
func prefixp(cidr string) policyv2.Alias { func prefixp(cidr string) policyv2.Alias {
prefix := netip.MustParsePrefix(cidr) 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. // 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. // usernameOwner returns a Username as an Owner for use in TagOwners policies.
// Specifies which users can assign and manage specific tags in ACL configurations. // Specifies which users can assign and manage specific tags in ACL configurations.
func usernameOwner(name string) policyv2.Owner { 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. // groupOwner returns a Group as an Owner for use in TagOwners policies.
// Specifies which groups can assign and manage specific tags in ACL configurations. // Specifies which groups can assign and manage specific tags in ACL configurations.
func groupOwner(name string) policyv2.Owner { 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. // usernameApprover returns a Username as an AutoApprover for subnet route policies.
// Specifies which users can automatically approve subnet route advertisements. // Specifies which users can automatically approve subnet route advertisements.
func usernameApprover(name string) policyv2.AutoApprover { 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. // groupApprover returns a Group as an AutoApprover for subnet route policies.
// Specifies which groups can automatically approve subnet route advertisements. // Specifies which groups can automatically approve subnet route advertisements.
func groupApprover(name string) policyv2.AutoApprover { 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. // tagApprover returns a Tag as an AutoApprover for subnet route policies.
// Specifies which tagged nodes can automatically approve subnet route advertisements. // Specifies which tagged nodes can automatically approve subnet route advertisements.
func tagApprover(name string) policyv2.AutoApprover { 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. // oidcMockUser creates a MockUser for OIDC authentication testing.

View file

@ -16,7 +16,7 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"sort" "slices"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -1232,8 +1232,8 @@ func (t *HeadscaleInContainer) ListNodes(
} }
} }
sort.Slice(ret, func(i, j int) bool { slices.SortFunc(ret, func(a, b *v1.Node) int {
return cmp.Compare(ret[i].GetId(), ret[j].GetId()) == -1 return cmp.Compare(a.GetId(), b.GetId())
}) })
return ret, nil return ret, nil

View file

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

View file

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