fix postgres migration issue with 0.24 (#2367)

* fix postgres migration issue with 0.24

Fixes #2351

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add postgres migration test for 2351

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2025-01-23 14:58:42 +01:00 committed by GitHub
parent 615ee5df75
commit 9e3f945eda
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 105 additions and 8 deletions

View file

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

View file

@ -478,6 +478,38 @@ func NewHeadscaleDatabase(
// populate the user with more interesting information. // populate the user with more interesting information.
ID: "202407191627", ID: "202407191627",
Migrate: func(tx *gorm.DB) error { 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{}) err := tx.AutoMigrate(&types.User{})
if err != nil { if err != nil {
return err return err

View file

@ -6,6 +6,7 @@ import (
"io" "io"
"net/netip" "net/netip"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"slices" "slices"
"sort" "sort"
@ -23,7 +24,10 @@ import (
"zgo.at/zcache/v2" "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 { ipp := func(p string) netip.Prefix {
return netip.MustParsePrefix(p) 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)
}
})
}
}

View file

@ -78,13 +78,11 @@ func newSQLiteTestDB() (*HSDatabase, error) {
func newPostgresTestDB(t *testing.T) *HSDatabase { func newPostgresTestDB(t *testing.T) *HSDatabase {
t.Helper() t.Helper()
var err error return newHeadscaleDBFromPostgresURL(t, newPostgresDBForTest(t))
tmpDir, err = os.MkdirTemp("", "headscale-db-test-*") }
if err != nil {
t.Fatal(err)
}
log.Printf("database path: %s", tmpDir+"/headscale_test.db") func newPostgresDBForTest(t *testing.T) *url.URL {
t.Helper()
ctx := context.Background() ctx := context.Background()
srv, err := postgrestest.Start(ctx) srv, err := postgrestest.Start(ctx)
@ -100,10 +98,16 @@ func newPostgresTestDB(t *testing.T) *HSDatabase {
t.Logf("created local postgres: %s", u) t.Logf("created local postgres: %s", u)
pu, _ := url.Parse(u) pu, _ := url.Parse(u)
return pu
}
func newHeadscaleDBFromPostgresURL(t *testing.T, pu *url.URL) *HSDatabase {
t.Helper()
pass, _ := pu.User.Password() pass, _ := pu.User.Password()
port, _ := strconv.Atoi(pu.Port()) port, _ := strconv.Atoi(pu.Port())
db, err = NewHeadscaleDatabase( db, err := NewHeadscaleDatabase(
types.DatabaseConfig{ types.DatabaseConfig{
Type: types.DatabasePostgres, Type: types.DatabasePostgres,
Postgres: types.PostgresConfig{ Postgres: types.PostgresConfig{

Binary file not shown.