Skip to content

Commit 84431f2

Browse files
committed
fix postgres migration issue with 0.24 (#2367)
* fix postgres migration issue with 0.24 Fixes #2351 Signed-off-by: Kristoffer Dalby <[email protected]> * add postgres migration test for 2351 Signed-off-by: Kristoffer Dalby <[email protected]> * update changelog Signed-off-by: Kristoffer Dalby <[email protected]> --------- Signed-off-by: Kristoffer Dalby <[email protected]>
1 parent 5164b27 commit 84431f2

File tree

5 files changed

+105
-8
lines changed

5 files changed

+105
-8
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Changes
66

7+
- Fix migration issue with user table for PostgreSQL
8+
[#2367](https://github.com/juanfont/headscale/pull/2367)
79
- Relax username validation to allow emails
810
[#2364](https://github.com/juanfont/headscale/pull/2364)
911
- Remove invalid routes and add stronger constraints for routes to avoid API panic

hscontrol/db/db.go

+32
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,38 @@ func NewHeadscaleDatabase(
478478
// populate the user with more interesting information.
479479
ID: "202407191627",
480480
Migrate: func(tx *gorm.DB) error {
481+
// Fix an issue where the automigration in GORM expected a constraint to
482+
// exists that didnt, and add the one it wanted.
483+
// Fixes https://github.com/juanfont/headscale/issues/2351
484+
if cfg.Type == types.DatabasePostgres {
485+
err := tx.Exec(`
486+
BEGIN;
487+
DO $$
488+
BEGIN
489+
IF NOT EXISTS (
490+
SELECT 1 FROM pg_constraint
491+
WHERE conname = 'uni_users_name'
492+
) THEN
493+
ALTER TABLE users ADD CONSTRAINT uni_users_name UNIQUE (name);
494+
END IF;
495+
END $$;
496+
497+
DO $$
498+
BEGIN
499+
IF EXISTS (
500+
SELECT 1 FROM pg_constraint
501+
WHERE conname = 'users_name_key'
502+
) THEN
503+
ALTER TABLE users DROP CONSTRAINT users_name_key;
504+
END IF;
505+
END $$;
506+
COMMIT;
507+
`).Error
508+
if err != nil {
509+
return fmt.Errorf("failed to rename constraint: %w", err)
510+
}
511+
}
512+
481513
err := tx.AutoMigrate(&types.User{})
482514
if err != nil {
483515
return err

hscontrol/db/db_test.go

+60-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io"
77
"net/netip"
88
"os"
9+
"os/exec"
910
"path/filepath"
1011
"slices"
1112
"sort"
@@ -23,7 +24,10 @@ import (
2324
"zgo.at/zcache/v2"
2425
)
2526

26-
func TestMigrations(t *testing.T) {
27+
// TestMigrationsSQLite is the main function for testing migrations,
28+
// we focus on SQLite correctness as it is the main database used in headscale.
29+
// All migrations that are worth testing should be added here.
30+
func TestMigrationsSQLite(t *testing.T) {
2731
ipp := func(p string) netip.Prefix {
2832
return netip.MustParsePrefix(p)
2933
}
@@ -375,3 +379,58 @@ func TestConstraints(t *testing.T) {
375379
})
376380
}
377381
}
382+
383+
func TestMigrationsPostgres(t *testing.T) {
384+
tests := []struct {
385+
name string
386+
dbPath string
387+
wantFunc func(*testing.T, *HSDatabase)
388+
}{
389+
{
390+
name: "user-idx-breaking",
391+
dbPath: "testdata/pre-24-postgresdb.pssql.dump",
392+
wantFunc: func(t *testing.T, h *HSDatabase) {
393+
users, err := Read(h.DB, func(rx *gorm.DB) ([]types.User, error) {
394+
return ListUsers(rx)
395+
})
396+
require.NoError(t, err)
397+
398+
for _, user := range users {
399+
assert.NotEmpty(t, user.Name)
400+
assert.Empty(t, user.ProfilePicURL)
401+
assert.Empty(t, user.Email)
402+
}
403+
},
404+
},
405+
}
406+
407+
for _, tt := range tests {
408+
t.Run(tt.name, func(t *testing.T) {
409+
u := newPostgresDBForTest(t)
410+
411+
pgRestorePath, err := exec.LookPath("pg_restore")
412+
if err != nil {
413+
t.Fatal("pg_restore not found in PATH. Please install it and ensure it is accessible.")
414+
}
415+
416+
// Construct the pg_restore command
417+
cmd := exec.Command(pgRestorePath, "--verbose", "--if-exists", "--clean", "--no-owner", "--dbname", u.String(), tt.dbPath)
418+
419+
// Set the output streams
420+
cmd.Stdout = os.Stdout
421+
cmd.Stderr = os.Stderr
422+
423+
// Execute the command
424+
err = cmd.Run()
425+
if err != nil {
426+
t.Fatalf("failed to restore postgres database: %s", err)
427+
}
428+
429+
db = newHeadscaleDBFromPostgresURL(t, u)
430+
431+
if tt.wantFunc != nil {
432+
tt.wantFunc(t, db)
433+
}
434+
})
435+
}
436+
}

hscontrol/db/suite_test.go

+11-7
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,11 @@ func newSQLiteTestDB() (*HSDatabase, error) {
7878
func newPostgresTestDB(t *testing.T) *HSDatabase {
7979
t.Helper()
8080

81-
var err error
82-
tmpDir, err = os.MkdirTemp("", "headscale-db-test-*")
83-
if err != nil {
84-
t.Fatal(err)
85-
}
81+
return newHeadscaleDBFromPostgresURL(t, newPostgresDBForTest(t))
82+
}
8683

87-
log.Printf("database path: %s", tmpDir+"/headscale_test.db")
84+
func newPostgresDBForTest(t *testing.T) *url.URL {
85+
t.Helper()
8886

8987
ctx := context.Background()
9088
srv, err := postgrestest.Start(ctx)
@@ -100,10 +98,16 @@ func newPostgresTestDB(t *testing.T) *HSDatabase {
10098
t.Logf("created local postgres: %s", u)
10199
pu, _ := url.Parse(u)
102100

101+
return pu
102+
}
103+
104+
func newHeadscaleDBFromPostgresURL(t *testing.T, pu *url.URL) *HSDatabase {
105+
t.Helper()
106+
103107
pass, _ := pu.User.Password()
104108
port, _ := strconv.Atoi(pu.Port())
105109

106-
db, err = NewHeadscaleDatabase(
110+
db, err := NewHeadscaleDatabase(
107111
types.DatabaseConfig{
108112
Type: types.DatabasePostgres,
109113
Postgres: types.PostgresConfig{
Binary file not shown.

0 commit comments

Comments
 (0)