From 109e41269273382b7a6805c8d3f5666b40416f30 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 10 May 2025 08:11:27 +0200 Subject: [PATCH] db: add back last_seen to the database Fixes #2574 Signed-off-by: Kristoffer Dalby --- cmd/headscale/cli/utils.go | 4 ++-- hscontrol/app.go | 4 ++-- hscontrol/db/db.go | 19 ++++++++++++++++++- hscontrol/db/node.go | 14 ++++++++++++++ hscontrol/poll.go | 4 ++++ hscontrol/types/node.go | 6 +----- 6 files changed, 41 insertions(+), 10 deletions(-) diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index ff1137be..0347c0a9 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -27,14 +27,14 @@ func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) { cfg, err := types.LoadServerConfig() if err != nil { return nil, fmt.Errorf( - "failed to load configuration while creating headscale instance: %w", + "loading configuration: %w", err, ) } app, err := hscontrol.NewHeadscale(cfg) if err != nil { - return nil, err + return nil, fmt.Errorf("creating new headscale: %w", err) } return app, nil diff --git a/hscontrol/app.go b/hscontrol/app.go index 3b4be52f..d62acb34 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -145,7 +145,7 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) { registrationCache, ) if err != nil { - return nil, err + return nil, fmt.Errorf("new database: %w", err) } app.ipAlloc, err = db.NewIPAllocator(app.db, cfg.PrefixV4, cfg.PrefixV6, cfg.IPAllocation) @@ -160,7 +160,7 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) { }) if err = app.loadPolicyManager(); err != nil { - return nil, fmt.Errorf("failed to load ACL policy: %w", err) + return nil, fmt.Errorf("loading ACL policy: %w", err) } var authProvider AuthProvider diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index d299771f..74f51ddc 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -672,7 +672,24 @@ AND auth_key_id NOT IN ( { ID: "202502171819", Migrate: func(tx *gorm.DB) error { - _ = tx.Migrator().DropColumn(&types.Node{}, "last_seen") + // This migration originally removed the last_seen column + // from the node table, but it was added back in + // 202505091439. + return nil + }, + Rollback: func(db *gorm.DB) error { return nil }, + }, + // Add back last_seen column to node table. + { + ID: "202505091439", + Migrate: func(tx *gorm.DB) error { + // Add back last_seen column to node table if it does not exist. + // This is a workaround for the fact that the last_seen column + // was removed in the 202502171819 migration, but only for some + // beta testers. + if !tx.Migrator().HasColumn(&types.Node{}, "last_seen") { + _ = tx.Migrator().AddColumn(&types.Node{}, "last_seen") + } return nil }, diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index ed9e1f73..c91687da 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -251,6 +251,20 @@ func SetApprovedRoutes( return nil } +// SetLastSeen sets a node's last seen field indicating that we +// have recently communicating with this node. +func (hsdb *HSDatabase) SetLastSeen(nodeID types.NodeID, lastSeen time.Time) error { + return hsdb.Write(func(tx *gorm.DB) error { + return SetLastSeen(tx, nodeID, lastSeen) + }) +} + +// SetLastSeen sets a node's last seen field indicating that we +// have recently communicating with this node. +func SetLastSeen(tx *gorm.DB, nodeID types.NodeID, lastSeen time.Time) error { + return tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("last_seen", lastSeen).Error +} + // RenameNode takes a Node struct and a new GivenName for the nodes // and renames it. If the name is not unique, it will return an error. func RenameNode(tx *gorm.DB, diff --git a/hscontrol/poll.go b/hscontrol/poll.go index e4178f43..763ab85b 100644 --- a/hscontrol/poll.go +++ b/hscontrol/poll.go @@ -409,6 +409,10 @@ func (h *Headscale) updateNodeOnlineStatus(online bool, node *types.Node) { change.LastSeen = &now } + if node.LastSeen != nil { + h.db.SetLastSeen(node.ID, *node.LastSeen) + } + ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-onlinestatus", node.Hostname) h.nodeNotifier.NotifyWithIgnore(ctx, types.UpdatePeerPatch(change), node.ID) } diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 2749237e..da185563 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -98,11 +98,7 @@ type Node struct { // LastSeen is when the node was last in contact with // headscale. It is best effort and not persisted. - LastSeen *time.Time `gorm:"-"` - - // DEPRECATED: Use the ApprovedRoutes field instead. - // TODO(kradalby): remove when ApprovedRoutes is used all over the code. - // Routes []Route `gorm:"constraint:OnDelete:CASCADE;"` + LastSeen *time.Time `gorm:"column:last_seen"` // ApprovedRoutes is a list of routes that the node is allowed to announce // as a subnet router. They are not necessarily the routes that the node