mirror of
https://github.com/hhftechnology/middleware-manager.git
synced 2026-07-12 01:18:24 +00:00
571 lines
No EOL
21 KiB
Go
571 lines
No EOL
21 KiB
Go
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
"context"
|
|
"sync"
|
|
|
|
"github.com/hhftechnology/middleware-manager/util"
|
|
)
|
|
|
|
// CleanupOptions contains options for controlling cleanup operations
|
|
type CleanupOptions struct {
|
|
DryRun bool // If true, logs what would be done without making changes
|
|
LogLevel int // 0=errors only, 1=basic info, 2=verbose
|
|
MaxDeleteBatch int // Maximum number of items to delete in one batch
|
|
ReapDisabled bool // If true, physically delete disabled resources
|
|
RecoverCorrupted bool // If true, attempt to recover corrupted resources
|
|
}
|
|
|
|
// DefaultCleanupOptions returns the default cleanup options
|
|
func DefaultCleanupOptions() CleanupOptions {
|
|
return CleanupOptions{
|
|
DryRun: false,
|
|
LogLevel: 1,
|
|
MaxDeleteBatch: 100,
|
|
ReapDisabled: false,
|
|
RecoverCorrupted: true,
|
|
}
|
|
}
|
|
|
|
// Add this function locally if util package doesn't exist
|
|
func normalizeID(id string) string {
|
|
// Extract the base name (everything before the first @)
|
|
baseName := id
|
|
if idx := strings.Index(id, "@"); idx > 0 {
|
|
baseName = id[:idx]
|
|
}
|
|
return baseName
|
|
}
|
|
|
|
// CleanupDuplicateServices - CORRECTED VERSION
|
|
func (db *DB) CleanupDuplicateServices(opts CleanupOptions) error {
|
|
if opts.LogLevel >= 1 {
|
|
log.Println("Starting cleanup of duplicate services...")
|
|
}
|
|
|
|
// Get all services
|
|
rows, err := db.Query("SELECT id, name, type, config FROM services")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to query services: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
// Map to track unique base names
|
|
type serviceInfo struct {
|
|
ID string
|
|
Config string
|
|
}
|
|
uniqueServices := make(map[string]serviceInfo)
|
|
var servicesToDelete []string
|
|
|
|
// Process each service - COMPLETE the duplicate detection logic
|
|
for rows.Next() {
|
|
var id, name, typ, configStr string
|
|
if err := rows.Scan(&id, &name, &typ, &configStr); err != nil {
|
|
return fmt.Errorf("failed to scan service: %w", err)
|
|
}
|
|
|
|
// Get normalized ID
|
|
normalizedID := normalizeID(id) // Use local function instead of util.NormalizeID
|
|
|
|
// If we've already seen this normalized ID, check which one to keep
|
|
if existing, found := uniqueServices[normalizedID]; found {
|
|
// Determine which one to keep:
|
|
// 1. Prefer versions without provider suffixes or with @file suffix
|
|
// 2. If both have same suffix type, keep the one with simpler/shorter ID
|
|
keepNew := false
|
|
|
|
existingHasSuffix := strings.Contains(existing.ID, "@")
|
|
newHasSuffix := strings.Contains(id, "@")
|
|
|
|
if existingHasSuffix && !newHasSuffix {
|
|
// Keep the one without suffix
|
|
keepNew = true
|
|
} else if !existingHasSuffix && newHasSuffix {
|
|
// Keep existing without suffix
|
|
keepNew = false
|
|
} else if strings.HasSuffix(id, "@file") && !strings.HasSuffix(existing.ID, "@file") {
|
|
// Prefer @file suffix
|
|
keepNew = true
|
|
} else if !strings.HasSuffix(id, "@file") && strings.HasSuffix(existing.ID, "@file") {
|
|
// Keep existing with @file
|
|
keepNew = false
|
|
} else {
|
|
// Both have same suffix type, keep the one with simpler ID
|
|
if len(existing.ID) > len(id) {
|
|
keepNew = true
|
|
}
|
|
}
|
|
|
|
if keepNew {
|
|
// The new one is better, mark the old one for deletion
|
|
if opts.LogLevel >= 2 {
|
|
log.Printf("Duplicate found: keeping %s, will delete %s", id, existing.ID)
|
|
}
|
|
servicesToDelete = append(servicesToDelete, existing.ID)
|
|
uniqueServices[normalizedID] = serviceInfo{id, configStr}
|
|
} else {
|
|
// The existing one is better, mark this one for deletion
|
|
if opts.LogLevel >= 2 {
|
|
log.Printf("Duplicate found: keeping %s, will delete %s", existing.ID, id)
|
|
}
|
|
servicesToDelete = append(servicesToDelete, id)
|
|
}
|
|
} else {
|
|
// First time seeing this normalized ID
|
|
uniqueServices[normalizedID] = serviceInfo{id, configStr}
|
|
}
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return fmt.Errorf("error iterating services: %w", err)
|
|
}
|
|
|
|
if len(servicesToDelete) == 0 {
|
|
if opts.LogLevel >= 1 {
|
|
log.Println("No duplicate services found.")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if opts.DryRun {
|
|
log.Printf("DRY RUN: Would delete %d duplicate services", len(servicesToDelete))
|
|
for _, id := range servicesToDelete {
|
|
log.Printf(" - %s", id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Use timeout transaction to prevent indefinite locks
|
|
ctx := context.Background()
|
|
timeout := 30 * time.Second
|
|
|
|
return db.WithTimeoutTransaction(ctx, timeout, func(tx *sql.Tx) error {
|
|
// Process in smaller batches to reduce lock time
|
|
batchSize := opts.MaxDeleteBatch
|
|
if batchSize <= 0 {
|
|
batchSize = 50 // Default batch size
|
|
}
|
|
|
|
for i := 0; i < len(servicesToDelete); i += batchSize {
|
|
end := i + batchSize
|
|
if end > len(servicesToDelete) {
|
|
end = len(servicesToDelete)
|
|
}
|
|
|
|
batch := servicesToDelete[i:end]
|
|
|
|
// Use batch DELETE with IN clause for better performance
|
|
if len(batch) > 1 {
|
|
placeholders := strings.Repeat("?,", len(batch)-1) + "?"
|
|
args := make([]interface{}, len(batch))
|
|
for i, id := range batch {
|
|
args[i] = id
|
|
}
|
|
|
|
// First remove relationships in batch
|
|
_, err := tx.Exec(
|
|
fmt.Sprintf("DELETE FROM resource_services WHERE service_id IN (%s)", placeholders),
|
|
args...,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete service relationships: %w", err)
|
|
}
|
|
|
|
// Then delete services in batch
|
|
_, err = tx.Exec(
|
|
fmt.Sprintf("DELETE FROM services WHERE id IN (%s)", placeholders),
|
|
args...,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete services: %w", err)
|
|
}
|
|
|
|
if opts.LogLevel >= 1 {
|
|
log.Printf("Deleted batch of %d services", len(batch))
|
|
}
|
|
} else {
|
|
// Single item - original logic
|
|
id := batch[0]
|
|
if _, err := tx.Exec("DELETE FROM resource_services WHERE service_id = ?", id); err != nil {
|
|
return fmt.Errorf("failed to delete resource_service references for %s: %w", id, err)
|
|
}
|
|
if _, err := tx.Exec("DELETE FROM services WHERE id = ?", id); err != nil {
|
|
return fmt.Errorf("failed to delete service %s: %w", id, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// CleanupManager - CORRECTED VERSION with proper DB reference
|
|
type CleanupManager struct {
|
|
db *DB // This should match your actual DB type
|
|
cleanupMutex sync.Mutex
|
|
isCleanupRunning bool
|
|
}
|
|
|
|
func NewCleanupManager(database *DB) *CleanupManager {
|
|
return &CleanupManager{
|
|
db: database,
|
|
cleanupMutex: sync.Mutex{},
|
|
isCleanupRunning: false,
|
|
}
|
|
}
|
|
|
|
func (cm *CleanupManager) PerformFullCleanup(opts CleanupOptions) error {
|
|
cm.cleanupMutex.Lock()
|
|
defer cm.cleanupMutex.Unlock()
|
|
|
|
if cm.isCleanupRunning {
|
|
return fmt.Errorf("cleanup already in progress")
|
|
}
|
|
|
|
cm.isCleanupRunning = true
|
|
defer func() {
|
|
cm.isCleanupRunning = false
|
|
}()
|
|
|
|
// Add warning log
|
|
if opts.LogLevel >= 1 {
|
|
log.Println("⚠️ Database cleanup starting - this may cause brief service interruptions")
|
|
}
|
|
|
|
// First clean up services
|
|
if err := cm.db.CleanupDuplicateServices(opts); err != nil {
|
|
return fmt.Errorf("service cleanup failed: %w", err)
|
|
}
|
|
|
|
// Then clean up resources
|
|
if err := cm.db.CleanupDuplicateResources(opts); err != nil {
|
|
return fmt.Errorf("resource cleanup failed: %w", err)
|
|
}
|
|
|
|
// Finally clean up orphaned relationships
|
|
if err := cm.db.CleanupOrphanedRelationships(opts); err != nil {
|
|
return fmt.Errorf("relationship cleanup failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CleanupDuplicateResources removes resource duplication from the database
|
|
func (db *DB) CleanupDuplicateResources(opts CleanupOptions) error {
|
|
if opts.LogLevel >= 1 {
|
|
log.Println("Starting cleanup of duplicate resources...")
|
|
}
|
|
|
|
// Get all resources
|
|
rows, err := db.Query("SELECT id, host, service_id, status FROM resources")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to query resources: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
// Map to track resources by normalized ID
|
|
type resourceInfo struct {
|
|
ID string
|
|
Host string
|
|
ServiceID string
|
|
Status string
|
|
}
|
|
|
|
// Group by host to find multiple resources for the same host
|
|
hostMap := make(map[string][]resourceInfo)
|
|
|
|
// Process each resource
|
|
for rows.Next() {
|
|
var id, host, serviceID, status string
|
|
if err := rows.Scan(&id, &host, &serviceID, &status); err != nil {
|
|
return fmt.Errorf("failed to scan resource: %w", err)
|
|
}
|
|
|
|
// Add to host map
|
|
hostMap[host] = append(hostMap[host], resourceInfo{
|
|
ID: id,
|
|
Host: host,
|
|
ServiceID: serviceID,
|
|
Status: status,
|
|
})
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return fmt.Errorf("error iterating resources: %w", err)
|
|
}
|
|
|
|
// Find hosts with multiple resources
|
|
var resourcesToDelete []string
|
|
var resourcesToActivate []string
|
|
|
|
for host, resources := range hostMap {
|
|
if len(resources) <= 1 {
|
|
continue // No duplicates
|
|
}
|
|
|
|
if opts.LogLevel >= 2 {
|
|
log.Printf("Found %d resources for host %s", len(resources), host)
|
|
}
|
|
|
|
// Sort resources by status (active first) and then by ID complexity
|
|
// We'll keep the active one with the simplest ID
|
|
activeResources := make([]resourceInfo, 0)
|
|
disabledResources := make([]resourceInfo, 0)
|
|
|
|
for _, res := range resources {
|
|
if res.Status == "active" {
|
|
activeResources = append(activeResources, res)
|
|
} else {
|
|
disabledResources = append(disabledResources, res)
|
|
}
|
|
}
|
|
|
|
// If there are multiple active resources, disable extras
|
|
if len(activeResources) > 1 {
|
|
// Sort to find the one to keep (prioritize simpler IDs)
|
|
bestID := ""
|
|
bestIdx := 0
|
|
|
|
for i, res := range activeResources {
|
|
normalizedID := util.NormalizeID(res.ID)
|
|
|
|
if bestID == "" {
|
|
bestID = normalizedID
|
|
bestIdx = i
|
|
} else {
|
|
// Prefer router-auth pattern for consistency
|
|
if strings.Contains(normalizedID, "-router-auth") &&
|
|
!strings.Contains(bestID, "-router-auth") {
|
|
bestID = normalizedID
|
|
bestIdx = i
|
|
} else if !strings.Contains(normalizedID, "-router-auth") &&
|
|
strings.Contains(bestID, "-router-auth") {
|
|
// Keep current best
|
|
} else if len(normalizedID) < len(bestID) {
|
|
// Prefer shorter/simpler IDs
|
|
bestID = normalizedID
|
|
bestIdx = i
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keep the best one, mark others for deletion
|
|
for i, res := range activeResources {
|
|
if i != bestIdx {
|
|
if opts.LogLevel >= 2 {
|
|
log.Printf(" - Will disable duplicate active resource: %s", res.ID)
|
|
}
|
|
resourcesToDelete = append(resourcesToDelete, res.ID)
|
|
} else if opts.LogLevel >= 2 {
|
|
log.Printf(" - Keeping active resource: %s", res.ID)
|
|
}
|
|
}
|
|
} else if len(activeResources) == 0 && len(disabledResources) > 0 && opts.RecoverCorrupted {
|
|
// No active resources, recover one
|
|
bestIdx := 0
|
|
bestID := ""
|
|
|
|
for i, res := range disabledResources {
|
|
normalizedID := util.NormalizeID(res.ID)
|
|
|
|
if bestID == "" {
|
|
bestID = normalizedID
|
|
bestIdx = i
|
|
} else if len(normalizedID) < len(bestID) {
|
|
// Prefer shorter/simpler IDs
|
|
bestID = normalizedID
|
|
bestIdx = i
|
|
}
|
|
}
|
|
|
|
// Activate the best one
|
|
if opts.LogLevel >= 2 {
|
|
log.Printf(" - Will activate resource: %s", disabledResources[bestIdx].ID)
|
|
}
|
|
resourcesToActivate = append(resourcesToActivate, disabledResources[bestIdx].ID)
|
|
|
|
// If reaping disabled resources, delete the rest
|
|
if opts.ReapDisabled {
|
|
for i, res := range disabledResources {
|
|
if i != bestIdx {
|
|
if opts.LogLevel >= 2 {
|
|
log.Printf(" - Will delete disabled resource: %s", res.ID)
|
|
}
|
|
resourcesToDelete = append(resourcesToDelete, res.ID)
|
|
}
|
|
}
|
|
}
|
|
} else if opts.ReapDisabled {
|
|
// Delete all disabled resources if ReapDisabled is true
|
|
for _, res := range disabledResources {
|
|
if opts.LogLevel >= 2 {
|
|
log.Printf(" - Will delete disabled resource: %s", res.ID)
|
|
}
|
|
resourcesToDelete = append(resourcesToDelete, res.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(resourcesToDelete) == 0 && len(resourcesToActivate) == 0 {
|
|
if opts.LogLevel >= 1 {
|
|
log.Println("No resources need cleanup.")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if opts.DryRun {
|
|
log.Printf("DRY RUN: Would delete %d resources and activate %d resources",
|
|
len(resourcesToDelete), len(resourcesToActivate))
|
|
return nil
|
|
}
|
|
|
|
// Process changes in a transaction
|
|
return db.WithTransaction(func(tx *sql.Tx) error {
|
|
// Activate resources that need activation
|
|
for _, id := range resourcesToActivate {
|
|
if opts.LogLevel >= 1 {
|
|
log.Printf("Activating resource: %s", id)
|
|
}
|
|
|
|
_, err := tx.Exec(
|
|
"UPDATE resources SET status = 'active', updated_at = ? WHERE id = ?",
|
|
time.Now(), id,
|
|
)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to activate resource %s: %w", id, err)
|
|
}
|
|
}
|
|
|
|
// Delete or disable resources
|
|
for _, id := range resourcesToDelete {
|
|
if opts.ReapDisabled {
|
|
// Physically delete the resource
|
|
if opts.LogLevel >= 1 {
|
|
log.Printf("Deleting resource: %s", id)
|
|
}
|
|
|
|
// First delete any middleware relationships
|
|
if _, err := tx.Exec("DELETE FROM resource_middlewares WHERE resource_id = ?", id); err != nil {
|
|
return fmt.Errorf("failed to delete resource_middlewares for %s: %w", id, err)
|
|
}
|
|
|
|
// Then delete any service relationships
|
|
if _, err := tx.Exec("DELETE FROM resource_services WHERE resource_id = ?", id); err != nil {
|
|
return fmt.Errorf("failed to delete resource_services for %s: %w", id, err)
|
|
}
|
|
|
|
// Finally delete the resource
|
|
if _, err := tx.Exec("DELETE FROM resources WHERE id = ?", id); err != nil {
|
|
return fmt.Errorf("failed to delete resource %s: %w", id, err)
|
|
}
|
|
} else {
|
|
// Just mark as disabled
|
|
if opts.LogLevel >= 1 {
|
|
log.Printf("Disabling resource: %s", id)
|
|
}
|
|
|
|
_, err := tx.Exec(
|
|
"UPDATE resources SET status = 'disabled', updated_at = ? WHERE id = ?",
|
|
time.Now(), id,
|
|
)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to disable resource %s: %w", id, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if opts.LogLevel >= 1 {
|
|
log.Printf("Resource cleanup complete. Deleted/disabled %d resources, activated %d resources",
|
|
len(resourcesToDelete), len(resourcesToActivate))
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// PerformFullCleanup runs a comprehensive cleanup of the database
|
|
func (db *DB) PerformFullCleanup(opts CleanupOptions) error {
|
|
// First clean up services
|
|
if err := db.CleanupDuplicateServices(opts); err != nil {
|
|
return fmt.Errorf("service cleanup failed: %w", err)
|
|
}
|
|
|
|
// Then clean up resources
|
|
if err := db.CleanupDuplicateResources(opts); err != nil {
|
|
return fmt.Errorf("resource cleanup failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CleanupOrphanedRelationships removes relationship rows that reference missing resources, services or middlewares.
|
|
func (db *DB) CleanupOrphanedRelationships(opts CleanupOptions) error {
|
|
if opts.LogLevel >= 1 {
|
|
log.Println("Starting cleanup of orphaned relationships...")
|
|
}
|
|
|
|
queries := []struct {
|
|
desc string
|
|
qry string
|
|
}{
|
|
{"orphaned resource_services by missing service", "SELECT COUNT(*) FROM resource_services rs LEFT JOIN services s ON rs.service_id = s.id WHERE s.id IS NULL"},
|
|
{"orphaned resource_services by missing resource", "SELECT COUNT(*) FROM resource_services rs LEFT JOIN resources r ON rs.resource_id = r.id WHERE r.id IS NULL"},
|
|
{"orphaned resource_middlewares by missing middleware", "SELECT COUNT(*) FROM resource_middlewares rm LEFT JOIN middlewares m ON rm.middleware_id = m.id WHERE m.id IS NULL"},
|
|
{"orphaned resource_middlewares by missing resource", "SELECT COUNT(*) FROM resource_middlewares rm LEFT JOIN resources r ON rm.resource_id = r.id WHERE r.id IS NULL"},
|
|
}
|
|
|
|
// Dry run: just report counts
|
|
if opts.DryRun {
|
|
for _, q := range queries {
|
|
var count int64
|
|
if err := db.QueryRow(q.qry).Scan(&count); err != nil {
|
|
// Non-fatal: log and continue
|
|
if opts.LogLevel >= 0 {
|
|
log.Printf("DRY RUN: failed to count %s: %v", q.desc, err)
|
|
}
|
|
continue
|
|
}
|
|
log.Printf("DRY RUN: %s: %d", q.desc, count)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Execute deletes in a transaction
|
|
return db.WithTransaction(func(tx *sql.Tx) error {
|
|
delQueries := []struct {
|
|
desc string
|
|
qry string
|
|
}{
|
|
{"delete resource_services with missing service", "DELETE FROM resource_services WHERE service_id NOT IN (SELECT id FROM services)"},
|
|
{"delete resource_services with missing resource", "DELETE FROM resource_services WHERE resource_id NOT IN (SELECT id FROM resources)"},
|
|
{"delete resource_middlewares with missing middleware", "DELETE FROM resource_middlewares WHERE middleware_id NOT IN (SELECT id FROM middlewares)"},
|
|
{"delete resource_middlewares with missing resource", "DELETE FROM resource_middlewares WHERE resource_id NOT IN (SELECT id FROM resources)"},
|
|
}
|
|
|
|
for _, dq := range delQueries {
|
|
res, err := tx.Exec(dq.qry)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to %s: %w", dq.desc, err)
|
|
}
|
|
if opts.LogLevel >= 1 {
|
|
if n, err := res.RowsAffected(); err == nil {
|
|
log.Printf("Deleted %d rows: %s", n, dq.desc)
|
|
} else {
|
|
log.Printf("Deleted rows (unknown count): %s", dq.desc)
|
|
}
|
|
}
|
|
}
|
|
|
|
if opts.LogLevel >= 1 {
|
|
log.Println("Orphaned relationship cleanup complete.")
|
|
}
|
|
return nil
|
|
})
|
|
} |