fix(backend): secure production startup and migrations
This commit is contained in:
@@ -67,6 +67,12 @@ func TestAutoMigrateUpgradesLegacyProjectsAndTagsWithoutLosingAssociations(t *te
|
||||
require.Error(t, database.Exec(`INSERT INTO senlin_agent_tags (project_id, name) VALUES (1, 'UI')`).Error)
|
||||
}
|
||||
|
||||
func TestMigrationAdvisoryLockIsRequiredOnlyForPostgres(t *testing.T) {
|
||||
require.True(t, migrationAdvisoryLockRequired("postgres"))
|
||||
require.False(t, migrationAdvisoryLockRequired("sqlite"))
|
||||
require.False(t, migrationAdvisoryLockRequired("mysql"))
|
||||
}
|
||||
|
||||
func projectIdentifiers(projects []SenlinAgentProject) []string {
|
||||
result := make([]string, 0, len(projects))
|
||||
for _, project := range projects {
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var DBService *gorm.DB
|
||||
|
||||
// migrationAdvisoryLockKey scopes the PostgreSQL session lock to SenlinAI schema changes.
|
||||
const migrationAdvisoryLockKey int64 = 0x53454e4c494e4149
|
||||
|
||||
func New(dsn string) error {
|
||||
// 启用 Gorm 跨驱动错误翻译,业务层无需解析 Postgres 错误文本或 SQLSTATE。
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{TranslateError: true})
|
||||
@@ -23,6 +28,29 @@ func New(dsn string) error {
|
||||
}
|
||||
|
||||
func AutoMigrate(database *gorm.DB) error {
|
||||
if !migrationAdvisoryLockRequired(database.Dialector.Name()) {
|
||||
return autoMigrateUnlocked(database)
|
||||
}
|
||||
// Connection pins every migration statement to one PostgreSQL session so the
|
||||
// advisory lock covers both Gorm's schema work and the versioned data repairs.
|
||||
return database.Connection(func(connection *gorm.DB) (resultErr error) {
|
||||
if err := connection.Exec("SELECT pg_advisory_lock(?)", migrationAdvisoryLockKey).Error; err != nil {
|
||||
return fmt.Errorf("acquire schema migration advisory lock: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := connection.Exec("SELECT pg_advisory_unlock(?)", migrationAdvisoryLockKey).Error; resultErr == nil && err != nil {
|
||||
resultErr = fmt.Errorf("release schema migration advisory lock: %w", err)
|
||||
}
|
||||
}()
|
||||
return autoMigrateUnlocked(connection)
|
||||
})
|
||||
}
|
||||
|
||||
func migrationAdvisoryLockRequired(dialect string) bool {
|
||||
return dialect == "postgres"
|
||||
}
|
||||
|
||||
func autoMigrateUnlocked(database *gorm.DB) error {
|
||||
if err := database.AutoMigrate(
|
||||
&SenlinAgentSchemaMigration{},
|
||||
&SenlinAgentUser{},
|
||||
|
||||
@@ -3,20 +3,15 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"senlinai-agent/backend/internal/testutil"
|
||||
)
|
||||
|
||||
func TestPostgresPing(t *testing.T) {
|
||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
||||
require.NotEmpty(t, dsn, "TEST_DATABASE_URL is required for integration tests and must point to an isolated database")
|
||||
|
||||
database, err := gorm.Open(postgres.Open(dsn), &gorm.Config{TranslateError: true})
|
||||
require.NoError(t, err)
|
||||
database := testutil.OpenIsolatedPostgres(t, &gorm.Config{TranslateError: true})
|
||||
sqlDB, err := database.DB()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, sqlDB.Ping())
|
||||
|
||||
Reference in New Issue
Block a user