db: backfill zero-time node expiry to NULL

Versions before 0.28 persisted a pointer to a zero time.Time as
'0001-01-01 00:00:00+00:00' in nodes.expiry rather than NULL. 0.29
reports those rows as expired. #3197 and #3199 stopped new writes;
this normalises the existing rows so the column once again means
"no expiry" when unset.

Fixes: #3284

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Shourya Gautam 2026-05-22 21:56:30 +05:30 committed by Kristoffer Dalby
parent 77ba225cdb
commit ea8fc72570
4 changed files with 159 additions and 1 deletions

View file

@ -144,6 +144,60 @@ func TestSQLiteMigrationAndDataValidation(t *testing.T) {
assert.NotContains(t, node7.Tags, "tag:forbidden", "node7 should NOT have tag:forbidden (unauthorized)")
},
},
// Test for the zero-time node expiry migration
// (202605221435-clear-zero-time-node-expiry). Pre-0.28 versions
// stored a zero time.Time as '0001-01-01 00:00:00+00:00' rather
// than NULL, which caused 0.29 to report those nodes as expired.
// Fixes: https://github.com/juanfont/headscale/issues/3284
{
dbPath: "testdata/sqlite/zero_time_expiry_migration_test.sql",
wantFunc: func(t *testing.T, hsdb *HSDatabase) {
t.Helper()
nodes, err := Read(hsdb.DB, func(rx *gorm.DB) (types.Nodes, error) {
return ListNodes(rx)
})
require.NoError(t, err)
require.Len(t, nodes, 5, "should have all 5 nodes")
byHostname := make(map[string]*types.Node, len(nodes))
for _, n := range nodes {
byHostname[n.Hostname] = n
}
// Node 1 had a zero-time expiry; should be cleared.
node1 := byHostname["node1"]
require.NotNil(t, node1, "node1 should exist")
assert.Nil(t, node1.Expiry, "node1 zero-time expiry should be cleared to NULL")
assert.False(t, node1.IsExpired(), "node1 should not be reported as expired")
// Node 2 already had NULL expiry; should still be NULL.
node2 := byHostname["node2"]
require.NotNil(t, node2, "node2 should exist")
assert.Nil(t, node2.Expiry, "node2 NULL expiry should be preserved")
assert.False(t, node2.IsExpired(), "node2 should not be reported as expired")
// Node 3 had a real future expiry; should be preserved.
node3 := byHostname["node3"]
require.NotNil(t, node3, "node3 should exist")
require.NotNil(t, node3.Expiry, "node3 future expiry should be preserved")
assert.Equal(t, 2099, node3.Expiry.UTC().Year(), "node3 expiry year should be 2099")
assert.False(t, node3.IsExpired(), "node3 with future expiry should not be expired")
// Node 4 had a real past expiry; should be preserved.
node4 := byHostname["node4"]
require.NotNil(t, node4, "node4 should exist")
require.NotNil(t, node4.Expiry, "node4 past expiry should be preserved")
assert.Equal(t, 2020, node4.Expiry.UTC().Year(), "node4 expiry year should be 2020")
assert.True(t, node4.IsExpired(), "node4 with past expiry should still be expired")
// Node 5 also had a zero-time expiry; should be cleared.
node5 := byHostname["node5"]
require.NotNil(t, node5, "node5 should exist")
assert.Nil(t, node5.Expiry, "node5 zero-time expiry should be cleared to NULL")
assert.False(t, node5.IsExpired(), "node5 should not be reported as expired")
},
},
}
for _, tt := range tests {