Smail/backend/config/database_migration.go
smail 31b0d88d03
Some checks are pending
Security Checks / security (push) Waiting to run
统一单数据库部署并完成三种数据库云端验证
2026-09-20 22:05:52 +08:00

154 lines
4.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package config
import (
"context"
"errors"
"strings"
"time"
)
var ErrDatabaseMigrationTarget = errors.New("database migration target is invalid")
type migrationAuditKey struct{}
// WithMigrationAuditWriter 仅供持有迁移目标连接的执行器设置迁移审计去向。
// 普通配置更新不读取该上下文值,HTTP 请求不能指定审计数据库。
func WithMigrationAuditWriter(ctx context.Context, writer ConfigAuditWriter) context.Context {
return context.WithValue(ctx, migrationAuditKey{}, writer)
}
// SwitchDatabase 提交已完成复制与校验的目标连接,保留旧库并标记重启。
// 普通配置编辑不能绕过本迁移入口切换数据库类型。
func (m *ConfigManager) SwitchDatabase(ctx context.Context, expectedGeneration uint64, target DatabaseConfig, actor AuditActor) error {
if err := contextError(ctx); err != nil {
return err
}
if m == nil || expectedGeneration == 0 {
return ErrManagerStorage
}
if actor.UserID == "" || actor.SessionID == "" {
return ErrAuthorizationRequired
}
if err := m.authorize(ctx, actor, ConfigOperationMigration, SectionRoot); err != nil {
return err
}
if err := contextError(ctx); err != nil {
return err
}
m.writeMu.Lock()
defer m.writeMu.Unlock()
m.mu.RLock()
current := cloneRuntimeConfig(m.current)
pending := m.pending != nil
m.mu.RUnlock()
if pending || expectedGeneration != current.Generation {
return ErrGenerationConflict
}
target = NormalizeDatabaseConfig(target)
if err := ValidateDatabaseConfig(target); err != nil {
return ErrDatabaseMigrationTarget
}
if SameDatabaseLocation(current.Root.Database, target) {
return ErrDatabaseMigrationTarget
}
candidate := cloneRuntimeConfig(current)
candidate.Root.Database = target
setCandidateGeneration(&candidate, current.Generation+1)
if err := ValidateRuntimeConfig(candidate, m.options); err != nil {
return err
}
if err := ValidateRuntimeSecrets(m.vault, candidate); err != nil {
return err
}
if err := m.snapshots.Validate(candidate); err != nil {
return err
}
if err := contextError(ctx); err != nil {
return err
}
backupStore := *m.transactions
backupStore.database = &target
if _, _, err := backupStore.CreateBackupIfDue(current, time.Now().UTC()); err != nil {
return err
}
if err := contextError(ctx); err != nil {
return err
}
if err := m.transactions.Commit(current, candidate); err != nil {
return err
}
if err := m.publishSnapshot(candidate); err != nil {
return err
}
now := time.Now().UTC()
m.mu.Lock()
m.current = cloneRuntimeConfig(candidate)
m.pending = nil
m.status = ConfigStatus{
SetupState: candidate.SetupState,
ActiveGeneration: candidate.Generation,
LastValidAt: &now,
PendingRestart: []ServiceName{ServiceDatabase},
}
m.mu.Unlock()
changedField := "root.database.name"
if target.Type == DatabaseTypeSQLite {
changedField = "root.database.localRef"
}
if !sameDatabaseConnection(current.Root.Database, target) {
changedField = "root.database"
}
event := ConfigAuditEvent{
Operation: ConfigOperationMigration,
Section: SectionRoot,
ActorID: actor.UserID,
ActorRole: actor.Role,
ActorName: actor.Name,
Changes: AuditChanges(current, candidate),
FromGeneration: current.Generation,
ToGeneration: candidate.Generation,
TargetGeneration: candidate.Generation,
ChangedFields: []string{changedField},
Result: "success",
CreatedAt: now,
}
if writer, ok := ctx.Value(migrationAuditKey{}).(ConfigAuditWriter); ok && writer != nil {
if err := writer.WriteConfigAudit(ctx, event); err != nil {
return errors.Join(ErrAuditWrite, err)
}
return nil
}
return m.writeAudit(ctx, event)
}
// SameDatabaseLocation 不把用户名或密码不同误认为不同数据库。
func SameDatabaseLocation(current, target DatabaseConfig) bool {
current = NormalizeDatabaseConfig(current)
target = NormalizeDatabaseConfig(target)
if current.Type != target.Type {
return false
}
if current.Type == DatabaseTypeSQLite {
return strings.EqualFold(current.LocalRef, target.LocalRef)
}
return strings.EqualFold(current.Host, target.Host) && current.Port == target.Port &&
strings.EqualFold(current.Name, target.Name)
}
func sameDatabaseConnection(current, target DatabaseConfig) bool {
if current.Type != target.Type ||
current.Host != target.Host ||
current.Port != target.Port ||
current.Username != target.Username ||
current.Password != target.Password {
return false
}
if current.Type == DatabaseTypeSQLite {
return true
}
return current.LocalRef == target.LocalRef
}