From cf6a606d74313b8b4dd4d5b07ee9b6ea61690624 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 27 Aug 2024 18:54:28 +0200 Subject: [PATCH] fix route table migration wiping routes 0.22 -> 0.23 (#2076) --- .github/workflows/test.yml | 2 +- hscontrol/db/db.go | 22 ++- hscontrol/db/db_test.go | 168 ++++++++++++++++++ hscontrol/db/node.go | 7 +- hscontrol/db/node_test.go | 14 +- ...3-to-0-23-0-routes-are-dropped-2063.sqlite | Bin 0 -> 98304 bytes ...0-23-0-routes-fail-foreign-key-2076.sqlite | Bin 0 -> 57344 bytes hscontrol/util/test.go | 6 +- integration/route_test.go | 4 +- 9 files changed, 204 insertions(+), 19 deletions(-) create mode 100644 hscontrol/db/db_test.go create mode 100644 hscontrol/db/testdata/0-22-3-to-0-23-0-routes-are-dropped-2063.sqlite create mode 100644 hscontrol/db/testdata/0-22-3-to-0-23-0-routes-fail-foreign-key-2076.sqlite diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b03fc434..f4659332 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,4 +34,4 @@ jobs: - name: Run tests if: steps.changed-files.outputs.files == 'true' - run: nix develop --check + run: nix develop --command -- gotestsum diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index 331dba54..3aaa7eeb 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -51,8 +51,8 @@ func NewHeadscaleDatabase( dbConn, gormigrate.DefaultOptions, []*gormigrate.Migration{ - // New migrations should be added as transactions at the end of this list. - // The initial commit here is quite messy, completely out of order and + // New migrations must be added as transactions at the end of this list. + // The initial migration here is quite messy, completely out of order and // has no versioning and is the tech debt of not having versioned migrations // prior to this point. This first migration is all DB changes to bring a DB // up to 0.23.0. @@ -123,9 +123,21 @@ func NewHeadscaleDatabase( } } - err = tx.AutoMigrate(&types.Route{}) - if err != nil { - return err + // Only run automigrate Route table if it does not exist. It has only been + // changed ones, when machines where renamed to nodes, which is covered + // further up. This whole initial integration is a mess and if AutoMigrate + // is ran on a 0.22 to 0.23 update, it will wipe all the routes. + if tx.Migrator().HasTable(&types.Route{}) && tx.Migrator().HasTable(&types.Node{}) { + err := tx.Exec("delete from routes where node_id not in (select id from nodes)").Error + if err != nil { + return err + } + } + if !tx.Migrator().HasTable(&types.Route{}) { + err = tx.AutoMigrate(&types.Route{}) + if err != nil { + return err + } } err = tx.AutoMigrate(&types.Node{}) diff --git a/hscontrol/db/db_test.go b/hscontrol/db/db_test.go new file mode 100644 index 00000000..b32d93ce --- /dev/null +++ b/hscontrol/db/db_test.go @@ -0,0 +1,168 @@ +package db + +import ( + "fmt" + "io" + "net/netip" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/juanfont/headscale/hscontrol/types" + "github.com/stretchr/testify/assert" + "gorm.io/gorm" +) + +func TestMigrations(t *testing.T) { + ipp := func(p string) types.IPPrefix { + return types.IPPrefix(netip.MustParsePrefix(p)) + } + r := func(id uint64, p string, a, e, i bool) types.Route { + return types.Route{ + NodeID: id, + Prefix: ipp(p), + Advertised: a, + Enabled: e, + IsPrimary: i, + } + } + tests := []struct { + dbPath string + wantFunc func(*testing.T, *HSDatabase) + wantErr string + }{ + { + dbPath: "testdata/0-22-3-to-0-23-0-routes-are-dropped-2063.sqlite", + wantFunc: func(t *testing.T, h *HSDatabase) { + routes, err := Read(h.DB, func(rx *gorm.DB) (types.Routes, error) { + return GetRoutes(rx) + }) + assert.NoError(t, err) + + assert.Len(t, routes, 10) + want := types.Routes{ + r(1, "0.0.0.0/0", true, true, false), + r(1, "::/0", true, true, false), + r(1, "10.9.110.0/24", true, true, true), + r(26, "172.100.100.0/24", true, true, true), + r(26, "172.100.100.0/24", true, false, false), + r(31, "0.0.0.0/0", true, true, false), + r(31, "0.0.0.0/0", true, false, false), + r(31, "::/0", true, true, false), + r(31, "::/0", true, false, false), + r(32, "192.168.0.24/32", true, true, true), + } + if diff := cmp.Diff(want, routes, cmpopts.IgnoreFields(types.Route{}, "Model", "Node"), cmp.Comparer(func(x, y types.IPPrefix) bool { + return x == y + })); diff != "" { + t.Errorf("TestMigrations() mismatch (-want +got):\n%s", diff) + } + }, + }, + { + dbPath: "testdata/0-22-3-to-0-23-0-routes-fail-foreign-key-2076.sqlite", + wantFunc: func(t *testing.T, h *HSDatabase) { + routes, err := Read(h.DB, func(rx *gorm.DB) (types.Routes, error) { + return GetRoutes(rx) + }) + assert.NoError(t, err) + + assert.Len(t, routes, 4) + want := types.Routes{ + // These routes exists, but have no nodes associated with them + // when the migration starts. + // r(1, "0.0.0.0/0", true, true, false), + // r(1, "::/0", true, true, false), + // r(3, "0.0.0.0/0", true, true, false), + // r(3, "::/0", true, true, false), + // r(5, "0.0.0.0/0", true, true, false), + // r(5, "::/0", true, true, false), + // r(6, "0.0.0.0/0", true, true, false), + // r(6, "::/0", true, true, false), + // r(6, "10.0.0.0/8", true, false, false), + // r(7, "0.0.0.0/0", true, true, false), + // r(7, "::/0", true, true, false), + // r(7, "10.0.0.0/8", true, false, false), + // r(9, "0.0.0.0/0", true, true, false), + // r(9, "::/0", true, true, false), + // r(9, "10.0.0.0/8", true, true, false), + // r(11, "0.0.0.0/0", true, true, false), + // r(11, "::/0", true, true, false), + // r(11, "10.0.0.0/8", true, true, true), + // r(12, "0.0.0.0/0", true, true, false), + // r(12, "::/0", true, true, false), + // r(12, "10.0.0.0/8", true, false, false), + // + // These nodes exists, so routes should be kept. + r(13, "10.0.0.0/8", true, false, false), + r(13, "0.0.0.0/0", true, true, false), + r(13, "::/0", true, true, false), + r(13, "10.18.80.2/32", true, true, true), + } + if diff := cmp.Diff(want, routes, cmpopts.IgnoreFields(types.Route{}, "Model", "Node"), cmp.Comparer(func(x, y types.IPPrefix) bool { + return x == y + })); diff != "" { + t.Errorf("TestMigrations() mismatch (-want +got):\n%s", diff) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.dbPath, func(t *testing.T) { + dbPath, err := testCopyOfDatabase(tt.dbPath) + if err != nil { + t.Fatalf("copying db for test: %s", err) + } + + hsdb, err := NewHeadscaleDatabase(types.DatabaseConfig{ + Type: "sqlite3", + Sqlite: types.SqliteConfig{ + Path: dbPath, + }, + }, "") + if err != nil && tt.wantErr != err.Error() { + t.Errorf("TestMigrations() unexpected error = %v, wantErr %v", err, tt.wantErr) + } + + if tt.wantFunc != nil { + tt.wantFunc(t, hsdb) + } + }) + } +} + +func testCopyOfDatabase(src string) (string, error) { + sourceFileStat, err := os.Stat(src) + if err != nil { + return "", err + } + + if !sourceFileStat.Mode().IsRegular() { + return "", fmt.Errorf("%s is not a regular file", src) + } + + source, err := os.Open(src) + if err != nil { + return "", err + } + defer source.Close() + + tmpDir, err := os.MkdirTemp("", "hsdb-test-*") + if err != nil { + return "", err + } + + fn := filepath.Base(src) + dst := filepath.Join(tmpDir, fn) + + destination, err := os.Create(dst) + if err != nil { + return "", err + } + defer destination.Close() + _, err = io.Copy(destination, source) + return dst, err +} diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index a2515ebf..a9e78a45 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -5,6 +5,7 @@ import ( "fmt" "net/netip" "sort" + "sync" "time" "github.com/juanfont/headscale/hscontrol/types" @@ -12,7 +13,6 @@ import ( "github.com/patrickmn/go-cache" "github.com/puzpuzpuz/xsync/v3" "github.com/rs/zerolog/log" - "github.com/sasha-s/go-deadlock" "gorm.io/gorm" "tailscale.com/tailcfg" "tailscale.com/types/key" @@ -724,7 +724,7 @@ func ExpireExpiredNodes(tx *gorm.DB, // It is used to delete ephemeral nodes that have disconnected and should be // cleaned up. type EphemeralGarbageCollector struct { - mu deadlock.Mutex + mu sync.Mutex deleteFunc func(types.NodeID) toBeDeleted map[types.NodeID]*time.Timer @@ -752,10 +752,9 @@ func (e *EphemeralGarbageCollector) Close() { // Schedule schedules a node for deletion after the expiry duration. func (e *EphemeralGarbageCollector) Schedule(nodeID types.NodeID, expiry time.Duration) { e.mu.Lock() - defer e.mu.Unlock() - timer := time.NewTimer(expiry) e.toBeDeleted[nodeID] = timer + e.mu.Unlock() go func() { select { diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index ad94f064..c83da120 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -609,12 +609,14 @@ func TestEphemeralGarbageCollectorOrder(t *testing.T) { }) go e.Start() - e.Schedule(1, 1*time.Second) - e.Schedule(2, 2*time.Second) - e.Schedule(3, 3*time.Second) - e.Schedule(4, 4*time.Second) - e.Cancel(2) - e.Cancel(4) + go e.Schedule(1, 1*time.Second) + go e.Schedule(2, 2*time.Second) + go e.Schedule(3, 3*time.Second) + go e.Schedule(4, 4*time.Second) + + time.Sleep(time.Second) + go e.Cancel(2) + go e.Cancel(4) time.Sleep(6 * time.Second) diff --git a/hscontrol/db/testdata/0-22-3-to-0-23-0-routes-are-dropped-2063.sqlite b/hscontrol/db/testdata/0-22-3-to-0-23-0-routes-are-dropped-2063.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..10e1aaec5ed56ab30e47570788d37fa634fa0d82 GIT binary patch literal 98304 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 U#P$CnnH8F!Eewpz&0+Z&0JNAL00000 literal 0 HcmV?d00001 diff --git a/hscontrol/db/testdata/0-22-3-to-0-23-0-routes-fail-foreign-key-2076.sqlite b/hscontrol/db/testdata/0-22-3-to-0-23-0-routes-fail-foreign-key-2076.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..dbe969623060bbe12f2daa1cd00008788f80380f GIT binary patch literal 57344 zcmWFz^vNtqRY=P(%1ta$FlG>7U}WTRP*7lCU|?imVBljw0Colj1{MStERV#+%4B5F z%iG1v|Av8+vxR}5fqyrz0nY~>Z*C8+5Y84ZO{{uG1xG_*Gz3ONU^E0qLtr!nMnhoO zhd`qe2fMhqIAfD?Nn%n?YH4w5Q8Ad{bq;cM3~^Nmadh%=RZs$pDJf_qWTqr2WagEm zrl%I^BqSH5CYGe8#3z;{D5NBoq?Tmnrs^b=7J%63qA964shDc>5_3}%6iQMnN)$r< zJOe{rbpnDseI0`$6}(*|A=YVXYI3rP+ln(XWag!$RumWJWR|4HCzh7vgZc4br^OqB zDZa)+c6M<|NycVduz!m3OG{FVAtVoqj}jn)2@pG|>80Gno|IB63NnilOEUBGkP;Y{AZS!#WfwO$ zXKYNy2!i;;VC8wr<;|wO0SXu&3 z�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