This commit is contained in:
Yvan
2026-09-04 14:07:09 +08:00
parent af1e194396
commit 43db5e5450
22 changed files with 416 additions and 134 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ func databaseDSN(c *config.Database, name string) (string, error) {
}
query := c.Config
if query == "" {
query = "timeout=5s&parseTime=True&loc=Local&charset=utf8mb4"
query = "timeout=5s&readTimeout=5s&writeTimeout=5s&parseTime=True&loc=Local&charset=utf8mb4"
}
return fmt.Sprintf("%s:%s@tcp(%s)/%s?%s", c.User, c.Password, net.JoinHostPort(host, port), name, query), nil
case "pgsql":
+26
View File
@@ -0,0 +1,26 @@
package data
import (
"strings"
"testing"
"kra/internal/config"
)
func TestMySQLDatabaseDSNIncludesNetworkTimeouts(t *testing.T) {
dsn, err := databaseDSN(&config.Database{
Driver: "mysql",
User: "root",
Password: "secret",
Host: "mysql",
Name: "kra",
}, "")
if err != nil {
t.Fatal(err)
}
for _, option := range []string{"timeout=5s", "readTimeout=5s", "writeTimeout=5s"} {
if !strings.Contains(dsn, option) {
t.Fatalf("DSN %q does not contain %q", dsn, option)
}
}
}
+8
View File
@@ -137,6 +137,14 @@ func (d *Data) InitializeDatabase(ctx context.Context, input *system.DatabaseCon
}
}
}()
releaseLock, err := acquireInitializationLock(ctx, candidate, database)
if err != nil {
return err
}
defer releaseLock()
if persistedDatabaseConfigured(d.runtime) {
return errors.New("数据库已初始化,无需重复初始化")
}
db := candidate.WithContext(ctx)
if err := migrateAll(db, d.catalog); err != nil {
return err
@@ -54,6 +54,23 @@ func TestInitializationStateTracksRealDatabaseConnection(t *testing.T) {
}
}
func TestPersistedDatabaseConfiguredReadsSharedRuntimeConfig(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.yaml")
if err := os.WriteFile(configPath, []byte("data:\n database:\n driver: mysql\n name: kra\n"), 0o600); err != nil {
t.Fatal(err)
}
runtime := config.NewStore(&config.Config{Admin: &config.Admin{ConfigPath: configPath}})
if !persistedDatabaseConfigured(runtime) {
t.Fatal("persisted database configuration was not detected")
}
if err := os.WriteFile(configPath, []byte("data:\n database:\n driver: mysql\n name: \"\"\n"), 0o600); err != nil {
t.Fatal(err)
}
if persistedDatabaseConfigured(runtime) {
t.Fatal("empty persisted database configuration was treated as initialized")
}
}
func TestPersistDatabaseConfigPreservesTemplateAndRotatesJWTKey(t *testing.T) {
root := t.TempDir()
configPath := filepath.Join(root, "config.yaml")
+64
View File
@@ -0,0 +1,64 @@
package data
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
"time"
configpkg "kra/internal/config"
"gorm.io/gorm"
)
// acquireInitializationLock serializes first-install work across backend Pods.
// The Kubernetes deployment uses MySQL, whose named lock is connection-scoped
// and therefore remains held while the candidate database is migrated and seeded.
func acquireInitializationLock(ctx context.Context, db *gorm.DB, database *configpkg.Database) (func(), error) {
if normalizedDriver(database.Driver) != "mysql" {
return func() {}, nil
}
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("open initialization lock connection: %w", err)
}
conn, err := sqlDB.Conn(ctx)
if err != nil {
return nil, fmt.Errorf("reserve initialization lock connection: %w", err)
}
lockName := initializationLockName(database)
var acquired sql.NullInt64
if err := conn.QueryRowContext(ctx, "SELECT GET_LOCK(?, ?)", lockName, 60).Scan(&acquired); err != nil {
_ = conn.Close()
return nil, fmt.Errorf("acquire database initialization lock: %w", err)
}
if !acquired.Valid || acquired.Int64 != 1 {
_ = conn.Close()
return nil, fmt.Errorf("database initialization lock is busy")
}
return func() {
releaseCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, _ = conn.ExecContext(releaseCtx, "SELECT RELEASE_LOCK(?)", lockName)
cancel()
_ = conn.Close()
}, nil
}
func initializationLockName(database *configpkg.Database) string {
identity := fmt.Sprintf("%s\x00%s\x00%s\x00%s", database.Driver, database.Host, database.Port, database.Name)
sum := sha256.Sum256([]byte(identity))
return "kra-init-" + hex.EncodeToString(sum[:])[:48]
}
func persistedDatabaseConfigured(runtime *configpkg.Store) bool {
if runtime == nil || runtime.ConfigPath() == "" {
return false
}
persisted, err := configpkg.Load(runtime.ConfigPath())
if err != nil || persisted == nil || persisted.Data == nil {
return false
}
return databaseConnectionConfigured(persisted.Data.Database)
}