From 9e3f945eda1b7e3152699fe35a77a53a6bbd7d9d Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 23 Jan 2025 14:58:42 +0100 Subject: [PATCH] fix postgres migration issue with 0.24 (#2367) * fix postgres migration issue with 0.24 Fixes #2351 Signed-off-by: Kristoffer Dalby * add postgres migration test for 2351 Signed-off-by: Kristoffer Dalby * update changelog Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- CHANGELOG.md | 2 + hscontrol/db/db.go | 32 +++++++++ hscontrol/db/db_test.go | 61 +++++++++++++++++- hscontrol/db/suite_test.go | 18 ++++-- .../db/testdata/pre-24-postgresdb.pssql.dump | Bin 0 -> 19869 bytes 5 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 hscontrol/db/testdata/pre-24-postgresdb.pssql.dump diff --git a/CHANGELOG.md b/CHANGELOG.md index 4122dc2c..a06a2ad1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ ### Changes +- Fix migration issue with user table for PostgreSQL + [#2367](https://github.com/juanfont/headscale/pull/2367) - Relax username validation to allow emails [#2364](https://github.com/juanfont/headscale/pull/2364) - Remove invalid routes and add stronger constraints for routes to avoid API panic diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index 553d7f0e..36955e22 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -478,6 +478,38 @@ func NewHeadscaleDatabase( // populate the user with more interesting information. ID: "202407191627", Migrate: func(tx *gorm.DB) error { + // Fix an issue where the automigration in GORM expected a constraint to + // exists that didnt, and add the one it wanted. + // Fixes https://github.com/juanfont/headscale/issues/2351 + if cfg.Type == types.DatabasePostgres { + err := tx.Exec(` +BEGIN; +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'uni_users_name' + ) THEN + ALTER TABLE users ADD CONSTRAINT uni_users_name UNIQUE (name); + END IF; +END $$; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'users_name_key' + ) THEN + ALTER TABLE users DROP CONSTRAINT users_name_key; + END IF; +END $$; +COMMIT; +`).Error + if err != nil { + return fmt.Errorf("failed to rename constraint: %w", err) + } + } + err := tx.AutoMigrate(&types.User{}) if err != nil { return err diff --git a/hscontrol/db/db_test.go b/hscontrol/db/db_test.go index c3d9a835..0672c252 100644 --- a/hscontrol/db/db_test.go +++ b/hscontrol/db/db_test.go @@ -6,6 +6,7 @@ import ( "io" "net/netip" "os" + "os/exec" "path/filepath" "slices" "sort" @@ -23,7 +24,10 @@ import ( "zgo.at/zcache/v2" ) -func TestMigrations(t *testing.T) { +// TestMigrationsSQLite is the main function for testing migrations, +// we focus on SQLite correctness as it is the main database used in headscale. +// All migrations that are worth testing should be added here. +func TestMigrationsSQLite(t *testing.T) { ipp := func(p string) netip.Prefix { return netip.MustParsePrefix(p) } @@ -375,3 +379,58 @@ func TestConstraints(t *testing.T) { }) } } + +func TestMigrationsPostgres(t *testing.T) { + tests := []struct { + name string + dbPath string + wantFunc func(*testing.T, *HSDatabase) + }{ + { + name: "user-idx-breaking", + dbPath: "testdata/pre-24-postgresdb.pssql.dump", + wantFunc: func(t *testing.T, h *HSDatabase) { + users, err := Read(h.DB, func(rx *gorm.DB) ([]types.User, error) { + return ListUsers(rx) + }) + require.NoError(t, err) + + for _, user := range users { + assert.NotEmpty(t, user.Name) + assert.Empty(t, user.ProfilePicURL) + assert.Empty(t, user.Email) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := newPostgresDBForTest(t) + + pgRestorePath, err := exec.LookPath("pg_restore") + if err != nil { + t.Fatal("pg_restore not found in PATH. Please install it and ensure it is accessible.") + } + + // Construct the pg_restore command + cmd := exec.Command(pgRestorePath, "--verbose", "--if-exists", "--clean", "--no-owner", "--dbname", u.String(), tt.dbPath) + + // Set the output streams + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // Execute the command + err = cmd.Run() + if err != nil { + t.Fatalf("failed to restore postgres database: %s", err) + } + + db = newHeadscaleDBFromPostgresURL(t, u) + + if tt.wantFunc != nil { + tt.wantFunc(t, db) + } + }) + } +} diff --git a/hscontrol/db/suite_test.go b/hscontrol/db/suite_test.go index fb7ce1df..e9c71823 100644 --- a/hscontrol/db/suite_test.go +++ b/hscontrol/db/suite_test.go @@ -78,13 +78,11 @@ func newSQLiteTestDB() (*HSDatabase, error) { func newPostgresTestDB(t *testing.T) *HSDatabase { t.Helper() - var err error - tmpDir, err = os.MkdirTemp("", "headscale-db-test-*") - if err != nil { - t.Fatal(err) - } + return newHeadscaleDBFromPostgresURL(t, newPostgresDBForTest(t)) +} - log.Printf("database path: %s", tmpDir+"/headscale_test.db") +func newPostgresDBForTest(t *testing.T) *url.URL { + t.Helper() ctx := context.Background() srv, err := postgrestest.Start(ctx) @@ -100,10 +98,16 @@ func newPostgresTestDB(t *testing.T) *HSDatabase { t.Logf("created local postgres: %s", u) pu, _ := url.Parse(u) + return pu +} + +func newHeadscaleDBFromPostgresURL(t *testing.T, pu *url.URL) *HSDatabase { + t.Helper() + pass, _ := pu.User.Password() port, _ := strconv.Atoi(pu.Port()) - db, err = NewHeadscaleDatabase( + db, err := NewHeadscaleDatabase( types.DatabaseConfig{ Type: types.DatabasePostgres, Postgres: types.PostgresConfig{ diff --git a/hscontrol/db/testdata/pre-24-postgresdb.pssql.dump b/hscontrol/db/testdata/pre-24-postgresdb.pssql.dump new file mode 100644 index 0000000000000000000000000000000000000000..7f8df28b30279ff139058f09fcc6de8c5e0648e1 GIT binary patch literal 19869 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 literal 0 HcmV?d00001