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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 F=>SYY(Ki49 literal 0 HcmV?d00001