mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 15:26:36 +08:00
[功能] 添加源站管理功能,包括源站的创建、更新、删除及列表展示
This commit is contained in:
@@ -4,7 +4,7 @@ import "time"
|
||||
|
||||
const (
|
||||
legacyDatabaseSchemaVersion = 1
|
||||
currentDatabaseSchemaVersion = 3
|
||||
currentDatabaseSchemaVersion = 4
|
||||
databaseSchemaVersionRowID = 1
|
||||
)
|
||||
|
||||
|
||||
@@ -9,10 +9,13 @@ import (
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/url"
|
||||
"openflare/common"
|
||||
"openflare/utils/security"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -36,6 +39,7 @@ func registeredModels() []any {
|
||||
&File{},
|
||||
&User{},
|
||||
&Option{},
|
||||
&Origin{},
|
||||
&ProxyRoute{},
|
||||
&ConfigVersion{},
|
||||
&Node{},
|
||||
@@ -307,6 +311,19 @@ func validateDatabaseSchemaV3(db *gorm.DB, backend string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDatabaseSchemaV4(db *gorm.DB, backend string) error {
|
||||
if err := validateDatabaseSchemaV3(db, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if !db.Migrator().HasTable(&Origin{}) {
|
||||
return fmt.Errorf("table origins is missing")
|
||||
}
|
||||
if !db.Migrator().HasColumn(&ProxyRoute{}, "origin_id") {
|
||||
return fmt.Errorf("column proxy_routes.origin_id is missing")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func renameLegacyObservabilityShardTables(db *gorm.DB) error {
|
||||
for _, baseTable := range shardedObservabilityBaseTables() {
|
||||
for _, table := range observabilityShardTables(baseTable) {
|
||||
@@ -549,6 +566,91 @@ func migrateObservabilityShardsToID(db *gorm.DB, backend string) error {
|
||||
return dropLegacyObservabilityShardTables(db)
|
||||
}
|
||||
|
||||
func normalizeOriginAddressForMigration(raw string) string {
|
||||
return strings.ToLower(strings.TrimSpace(raw))
|
||||
}
|
||||
|
||||
func extractOriginAddressForMigration(rawURL string) string {
|
||||
parsed, err := url.ParseRequestURI(strings.TrimSpace(rawURL))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return normalizeOriginAddressForMigration(parsed.Hostname())
|
||||
}
|
||||
|
||||
func backfillOriginsFromProxyRoutes(db *gorm.DB) error {
|
||||
if db == nil {
|
||||
return fmt.Errorf("database handle is nil")
|
||||
}
|
||||
if !db.Migrator().HasTable(&Origin{}) || !db.Migrator().HasTable(&ProxyRoute{}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var routes []ProxyRoute
|
||||
if err := db.Order("id asc").Find(&routes).Error; err != nil {
|
||||
return fmt.Errorf("list proxy routes for origin backfill failed: %w", err)
|
||||
}
|
||||
|
||||
type originSeed struct {
|
||||
ID uint
|
||||
Address string
|
||||
}
|
||||
|
||||
originByAddress := make(map[string]originSeed)
|
||||
var origins []Origin
|
||||
if err := db.Order("id asc").Find(&origins).Error; err != nil {
|
||||
return fmt.Errorf("list origins for backfill failed: %w", err)
|
||||
}
|
||||
for _, origin := range origins {
|
||||
address := normalizeOriginAddressForMigration(origin.Address)
|
||||
if address == "" {
|
||||
continue
|
||||
}
|
||||
originByAddress[address] = originSeed{ID: origin.ID, Address: address}
|
||||
}
|
||||
|
||||
for _, route := range routes {
|
||||
address := extractOriginAddressForMigration(route.OriginURL)
|
||||
if address == "" {
|
||||
continue
|
||||
}
|
||||
origin, ok := originByAddress[address]
|
||||
if !ok {
|
||||
name := address
|
||||
if ip := net.ParseIP(address); ip != nil {
|
||||
name = ip.String()
|
||||
}
|
||||
record := Origin{
|
||||
Name: name,
|
||||
Address: address,
|
||||
Remark: "",
|
||||
}
|
||||
if err := db.Create(&record).Error; err != nil {
|
||||
return fmt.Errorf("create origin for address %s failed: %w", address, err)
|
||||
}
|
||||
origin = originSeed{ID: record.ID, Address: address}
|
||||
originByAddress[address] = origin
|
||||
}
|
||||
if route.OriginID != nil && *route.OriginID == origin.ID {
|
||||
continue
|
||||
}
|
||||
if err := db.Model(&ProxyRoute{}).
|
||||
Where("id = ?", route.ID).
|
||||
Update("origin_id", origin.ID).Error; err != nil {
|
||||
return fmt.Errorf("backfill proxy route %d origin_id failed: %w", route.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateOriginsSchema(db *gorm.DB, backend string) error {
|
||||
if err := applyCurrentSchema(db, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
return backfillOriginsFromProxyRoutes(db)
|
||||
}
|
||||
|
||||
func databaseSchemaMigrations() []databaseSchemaMigration {
|
||||
return []databaseSchemaMigration{
|
||||
{
|
||||
@@ -563,6 +665,12 @@ func databaseSchemaMigrations() []databaseSchemaMigration {
|
||||
migrate: migrateObservabilityShardsToID,
|
||||
validate: validateDatabaseSchemaV3,
|
||||
},
|
||||
{
|
||||
fromVersion: 3,
|
||||
toVersion: 4,
|
||||
migrate: migrateOriginsSchema,
|
||||
validate: validateDatabaseSchemaV4,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -575,6 +683,19 @@ func databaseSchemaMigrationMap() map[int]databaseSchemaMigration {
|
||||
}
|
||||
|
||||
func runDatabaseSchemaMigration(db *gorm.DB, backend string, migration databaseSchemaMigration) error {
|
||||
if backend == "sqlite" {
|
||||
if err := migration.migrate(db, backend); err != nil {
|
||||
return fmt.Errorf("migrate database schema from v%d to v%d failed: %w", migration.fromVersion, migration.toVersion, err)
|
||||
}
|
||||
if err := migration.validate(db, backend); err != nil {
|
||||
return fmt.Errorf("validate database schema v%d failed: %w", migration.toVersion, err)
|
||||
}
|
||||
if err := saveDatabaseSchemaVersion(db, migration.toVersion); err != nil {
|
||||
return fmt.Errorf("persist database schema version v%d failed: %w", migration.toVersion, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := migration.migrate(tx, backend); err != nil {
|
||||
return fmt.Errorf("migrate database schema from v%d to v%d failed: %w", migration.fromVersion, migration.toVersion, err)
|
||||
@@ -614,10 +735,13 @@ func initializeFreshDatabaseSchema(db *gorm.DB, backend string) error {
|
||||
if err := applyCurrentSchema(db, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := backfillOriginsFromProxyRoutes(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := migrateSQLiteDataIfNeeded(db, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateDatabaseSchemaV3(db, backend); err != nil {
|
||||
if err := validateDatabaseSchemaV4(db, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
return saveDatabaseSchemaVersion(db, currentDatabaseSchemaVersion)
|
||||
|
||||
@@ -409,6 +409,61 @@ func TestEnsureDatabaseSchemaUpToDateMigratesObservabilityShardsToID(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateOriginsSchemaBackfillsOrigins(t *testing.T) {
|
||||
db := openBareTestSQLiteDB(t, "legacy-origins.db")
|
||||
if err := registerSharding(db, "sqlite"); err != nil {
|
||||
t.Fatalf("register sharding: %v", err)
|
||||
}
|
||||
if err := applyCurrentSchema(db, "sqlite"); err != nil {
|
||||
t.Fatalf("applyCurrentSchema: %v", err)
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
route := &ProxyRoute{
|
||||
Domain: "app.example.com",
|
||||
OriginURL: "https://origin-a.internal:8443/api",
|
||||
Upstreams: `["https://origin-a.internal:8443/api"]`,
|
||||
Enabled: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := db.Create(route).Error; err != nil {
|
||||
t.Fatalf("seed proxy route: %v", err)
|
||||
}
|
||||
if err := db.Exec(`DELETE FROM origins`).Error; err != nil {
|
||||
t.Fatalf("clear origins: %v", err)
|
||||
}
|
||||
if err := db.Model(&ProxyRoute{}).Where("id = ?", route.ID).Update("origin_id", nil).Error; err != nil {
|
||||
t.Fatalf("clear route origin_id: %v", err)
|
||||
}
|
||||
|
||||
if err := backfillOriginsFromProxyRoutes(db); err != nil {
|
||||
t.Fatalf("backfillOriginsFromProxyRoutes: %v", err)
|
||||
}
|
||||
|
||||
if !db.Migrator().HasTable(&Origin{}) {
|
||||
t.Fatal("expected origins table to exist")
|
||||
}
|
||||
if !db.Migrator().HasColumn(&ProxyRoute{}, "origin_id") {
|
||||
t.Fatal("expected proxy_routes.origin_id column to exist")
|
||||
}
|
||||
|
||||
reloadedRoute := &ProxyRoute{}
|
||||
if err := db.First(reloadedRoute, route.ID).Error; err != nil {
|
||||
t.Fatalf("query proxy route: %v", err)
|
||||
}
|
||||
if reloadedRoute.OriginID == nil || *reloadedRoute.OriginID == 0 {
|
||||
t.Fatal("expected migrated route to be linked to a backfilled origin")
|
||||
}
|
||||
|
||||
origin := &Origin{}
|
||||
if err := db.First(origin, *reloadedRoute.OriginID).Error; err != nil {
|
||||
t.Fatalf("query origin: %v", err)
|
||||
}
|
||||
if origin.Address != "origin-a.internal" {
|
||||
t.Fatalf("unexpected backfilled origin address: %s", origin.Address)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunDatabaseSchemaMigrationDoesNotAdvanceVersionWhenValidationFails(t *testing.T) {
|
||||
db := openBareTestSQLiteDB(t, "failed-validation.db")
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type Origin struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"size:255;not null"`
|
||||
Address string `json:"address" gorm:"uniqueIndex;size:255;not null"`
|
||||
Remark string `json:"remark" gorm:"size:255"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type OriginRouteCount struct {
|
||||
OriginID uint `json:"origin_id"`
|
||||
RouteCount int64 `json:"route_count"`
|
||||
}
|
||||
|
||||
func ListOrigins() (origins []*Origin, err error) {
|
||||
err = DB.Order("id desc").Find(&origins).Error
|
||||
return origins, err
|
||||
}
|
||||
|
||||
func GetOriginByID(id uint) (*Origin, error) {
|
||||
origin := &Origin{}
|
||||
err := DB.First(origin, id).Error
|
||||
return origin, err
|
||||
}
|
||||
|
||||
func GetOriginByAddress(address string) (*Origin, error) {
|
||||
origin := &Origin{}
|
||||
err := DB.Where("address = ?", address).First(origin).Error
|
||||
return origin, err
|
||||
}
|
||||
|
||||
func ListOriginRouteCounts() ([]OriginRouteCount, error) {
|
||||
result := make([]OriginRouteCount, 0)
|
||||
err := DB.Model(&ProxyRoute{}).
|
||||
Select("origin_id, COUNT(*) AS route_count").
|
||||
Where("origin_id IS NOT NULL").
|
||||
Group("origin_id").
|
||||
Scan(&result).Error
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (origin *Origin) Insert() error {
|
||||
return DB.Create(origin).Error
|
||||
}
|
||||
|
||||
func (origin *Origin) Update() error {
|
||||
return DB.Save(origin).Error
|
||||
}
|
||||
|
||||
func (origin *Origin) Delete() error {
|
||||
return DB.Delete(origin).Error
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import "time"
|
||||
type ProxyRoute struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Domain string `json:"domain" gorm:"uniqueIndex;size:255;not null"`
|
||||
OriginID *uint `json:"origin_id" gorm:"index"`
|
||||
OriginURL string `json:"origin_url" gorm:"size:2048;not null"`
|
||||
OriginHost string `json:"origin_host" gorm:"size:255"`
|
||||
Upstreams string `json:"upstreams" gorm:"type:text;not null;default:'[]'"`
|
||||
@@ -37,6 +38,11 @@ func GetProxyRouteByID(id uint) (*ProxyRoute, error) {
|
||||
return route, err
|
||||
}
|
||||
|
||||
func ListProxyRoutesByOriginID(originID uint) (routes []*ProxyRoute, err error) {
|
||||
err = DB.Where("origin_id = ?", originID).Order("id desc").Find(&routes).Error
|
||||
return routes, err
|
||||
}
|
||||
|
||||
func (route *ProxyRoute) Insert() error {
|
||||
return DB.Create(route).Error
|
||||
}
|
||||
@@ -44,6 +50,7 @@ func (route *ProxyRoute) Insert() error {
|
||||
func (route *ProxyRoute) Update() error {
|
||||
return DB.Model(&ProxyRoute{}).Where("id = ?", route.ID).Updates(map[string]any{
|
||||
"domain": route.Domain,
|
||||
"origin_id": route.OriginID,
|
||||
"origin_url": route.OriginURL,
|
||||
"origin_host": route.OriginHost,
|
||||
"upstreams": route.Upstreams,
|
||||
|
||||
Reference in New Issue
Block a user