all: upgrade to Go 1.26rc2 and modernize codebase

This commit upgrades the codebase from Go 1.25.5 to Go 1.26rc2 and
adopts new language features.

Toolchain updates:
- go.mod: go 1.25.5 → go 1.26rc2
- flake.nix: buildGo125Module → buildGo126Module, go_1_25 → go_1_26
- flake.nix: build golangci-lint from source with Go 1.26
- Dockerfile.integration: golang:1.25-trixie → golang:1.26rc2-trixie
- Dockerfile.tailscale-HEAD: golang:1.25-alpine → golang:1.26rc2-alpine
- Dockerfile.derper: golang:alpine → golang:1.26rc2-alpine
- .goreleaser.yml: go mod tidy -compat=1.25 → -compat=1.26
- cmd/hi/run.go: fallback Go version 1.25 → 1.26rc2
- .pre-commit-config.yaml: simplify golangci-lint hook entry

Code modernization using Go 1.26 features:
- Replace tsaddr.SortPrefixes with slices.SortFunc + netip.Prefix.Compare
- Replace ptr.To(x) with new(x) syntax
- Replace errors.As with errors.AsType[T]

Lint rule updates:
- Add forbidigo rules to prevent regression to old patterns
This commit is contained in:
Kristoffer Dalby 2026-02-06 21:39:35 +00:00
parent 20dff82f95
commit 0f6d312ada
50 changed files with 508 additions and 521 deletions

View file

@ -8,7 +8,6 @@ import (
"github.com/juanfont/headscale/hscontrol/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tailscale.com/types/ptr"
)
// TestEphemeralNodeDeleteWithConcurrentUpdate tests the race condition where UpdateNode and DeleteNode
@ -55,7 +54,7 @@ func TestEphemeralNodeDeleteWithConcurrentUpdate(t *testing.T) {
go func() {
updatedNode, updateOk = store.UpdateNode(node.ID, func(n *types.Node) {
n.LastSeen = ptr.To(time.Now())
n.LastSeen = new(time.Now())
})
done <- true
@ -114,7 +113,7 @@ func TestUpdateNodeReturnsInvalidWhenDeletedInSameBatch(t *testing.T) {
// Start UpdateNode in goroutine - it will queue and wait for batch
go func() {
node, ok := store.UpdateNode(node.ID, func(n *types.Node) {
n.LastSeen = ptr.To(time.Now())
n.LastSeen = new(time.Now())
})
resultChan <- struct {
node types.NodeView
@ -165,7 +164,7 @@ func TestPersistNodeToDBPreventsRaceCondition(t *testing.T) {
// Simulate UpdateNode being called
updatedNode, ok := store.UpdateNode(node.ID, func(n *types.Node) {
n.LastSeen = ptr.To(time.Now())
n.LastSeen = new(time.Now())
})
require.True(t, ok, "UpdateNode should succeed")
require.True(t, updatedNode.Valid(), "UpdateNode should return valid node")
@ -234,7 +233,7 @@ func TestEphemeralNodeLogoutRaceCondition(t *testing.T) {
// Goroutine 1: UpdateNode (simulates UpdateNodeFromMapRequest)
go func() {
updatedNode, updateOk = store.UpdateNode(ephemeralNode.ID, func(n *types.Node) {
n.LastSeen = ptr.To(time.Now())
n.LastSeen = new(time.Now())
})
done <- true
@ -310,7 +309,7 @@ func TestUpdateNodeFromMapRequestEphemeralLogoutSequence(t *testing.T) {
go func() {
node, ok := store.UpdateNode(ephemeralNode.ID, func(n *types.Node) {
n.LastSeen = ptr.To(time.Now())
n.LastSeen = new(time.Now())
endpoint := netip.MustParseAddrPort("10.0.0.1:41641")
n.Endpoints = []netip.AddrPort{endpoint}
})
@ -380,7 +379,7 @@ func TestUpdateNodeDeletedInSameBatchReturnsInvalid(t *testing.T) {
go func() {
updatedNode, ok := store.UpdateNode(node.ID, func(n *types.Node) {
n.LastSeen = ptr.To(time.Now())
n.LastSeen = new(time.Now())
})
updateDone <- struct {
node types.NodeView
@ -435,7 +434,7 @@ func TestPersistNodeToDBChecksNodeStoreBeforePersist(t *testing.T) {
// UpdateNode returns a node
updatedNode, ok := store.UpdateNode(ephemeralNode.ID, func(n *types.Node) {
n.LastSeen = ptr.To(time.Now())
n.LastSeen = new(time.Now())
})
require.True(t, ok, "UpdateNode should succeed")
require.True(t, updatedNode.Valid(), "updated node should be valid")

View file

@ -13,7 +13,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tailscale.com/types/key"
"tailscale.com/types/ptr"
)
func TestSnapshotFromNodes(t *testing.T) {
@ -175,7 +174,7 @@ func createTestNode(nodeID types.NodeID, userID uint, username, hostname string)
DiscoKey: discoKey.Public(),
Hostname: hostname,
GivenName: hostname,
UserID: ptr.To(userID),
UserID: new(userID),
User: &types.User{
Name: username,
DisplayName: username,
@ -883,7 +882,7 @@ func createConcurrentTestNode(id types.NodeID, hostname string) types.Node {
Hostname: hostname,
MachineKey: machineKey.Public(),
NodeKey: nodeKey.Public(),
UserID: ptr.To(uint(1)),
UserID: new(uint(1)),
User: &types.User{
Name: "concurrent-test-user",
},

View file

@ -27,10 +27,8 @@ import (
"github.com/rs/zerolog/log"
"golang.org/x/sync/errgroup"
"gorm.io/gorm"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/ptr"
"tailscale.com/types/views"
zcache "zgo.at/zcache/v2"
)
@ -135,7 +133,7 @@ func NewState(cfg *types.Config) (*State, error) {
// On startup, all nodes should be marked as offline until they reconnect
// This ensures we don't have stale online status from previous runs
for _, node := range nodes {
node.IsOnline = ptr.To(false)
node.IsOnline = new(false)
}
users, err := db.ListUsers()
@ -479,7 +477,7 @@ func (s *State) Connect(id types.NodeID) []change.Change {
// the NodeStore already reflects the correct online status for full map generation.
// now := time.Now()
node, ok := s.nodeStore.UpdateNode(id, func(n *types.Node) {
n.IsOnline = ptr.To(true)
n.IsOnline = new(true)
// n.LastSeen = ptr.To(now)
})
if !ok {
@ -507,9 +505,9 @@ func (s *State) Disconnect(id types.NodeID) ([]change.Change, error) {
now := time.Now()
node, ok := s.nodeStore.UpdateNode(id, func(n *types.Node) {
n.LastSeen = ptr.To(now)
n.LastSeen = new(now)
// NodeStore is the source of truth for all node state including online status.
n.IsOnline = ptr.To(false)
n.IsOnline = new(false)
})
if !ok {
@ -805,7 +803,7 @@ func (s *State) BackfillNodeIPs() ([]string, error) {
// Preserve online status and NetInfo when refreshing from database
existingNode, exists := s.nodeStore.GetNode(node.ID)
if exists && existingNode.Valid() {
node.IsOnline = ptr.To(existingNode.IsOnline().Get())
node.IsOnline = new(existingNode.IsOnline().Get())
// TODO(kradalby): We should ensure we use the same hostinfo and node merge semantics
// when a node re-registers as we do when it sends a map request (UpdateNodeFromMapRequest).
@ -1183,8 +1181,8 @@ func (s *State) applyAuthNodeUpdate(params authNodeUpdateParams) (types.NodeView
)
node.Endpoints = params.RegEntry.Node.Endpoints
node.IsOnline = ptr.To(false)
node.LastSeen = ptr.To(time.Now())
node.IsOnline = new(false)
node.LastSeen = new(time.Now())
// Set RegisterMethod - for conversion this is the new method,
// for reauth we preserve the existing one from regEntry
@ -1286,7 +1284,7 @@ func (s *State) createAndSaveNewNode(params newNodeParams) (types.NodeView, erro
DiscoKey: params.DiscoKey,
Hostinfo: params.Hostinfo,
Endpoints: params.Endpoints,
LastSeen: ptr.To(time.Now()),
LastSeen: new(time.Now()),
RegisterMethod: params.RegisterMethod,
Expiry: params.Expiry,
}
@ -1822,8 +1820,8 @@ func (s *State) HandleNodeFromPreAuthKey(
// Only update AuthKey reference
node.AuthKey = pak
node.AuthKeyID = &pak.ID
node.IsOnline = ptr.To(false)
node.LastSeen = ptr.To(time.Now())
node.IsOnline = new(false)
node.LastSeen = new(time.Now())
// Tagged nodes keep their existing expiry (disabled).
// User-owned nodes update expiry from the client request.
@ -2281,8 +2279,8 @@ func routesChanged(oldNode types.NodeView, newHI *tailcfg.Hostinfo) bool {
newRoutes = []netip.Prefix{}
}
tsaddr.SortPrefixes(oldRoutes)
tsaddr.SortPrefixes(newRoutes)
slices.SortFunc(oldRoutes, netip.Prefix.Compare)
slices.SortFunc(newRoutes, netip.Prefix.Compare)
return !slices.Equal(oldRoutes, newRoutes)
}