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 b03fc43..f465933 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 331dba5..3aaa7ee 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 0000000..b32d93c --- /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 a2515eb..a9e78a4 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 ad94f06..c83da12 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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 G_5TC3m>mEB 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 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