329 lines
12 KiB
Go
329 lines
12 KiB
Go
package deployment
|
|
|
|
import (
|
|
"bamort/config"
|
|
"bamort/deployment/backup"
|
|
"bamort/deployment/migrations"
|
|
"bamort/deployment/version"
|
|
"bamort/logger"
|
|
"bamort/transfer"
|
|
"fmt"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// DeploymentOrchestrator coordinates the full deployment process
|
|
type DeploymentOrchestrator struct {
|
|
DB *gorm.DB
|
|
}
|
|
|
|
// DeploymentReport contains the results of a deployment
|
|
type DeploymentReport struct {
|
|
Success bool
|
|
StartTime time.Time
|
|
EndTime time.Time
|
|
Duration time.Duration
|
|
BackupCreated bool
|
|
BackupPath string
|
|
MigrationsRun int
|
|
ValidationPassed bool
|
|
Errors []string
|
|
Warnings []string
|
|
}
|
|
|
|
// NewOrchestrator creates a new deployment orchestrator
|
|
func NewOrchestrator(db *gorm.DB) *DeploymentOrchestrator {
|
|
return &DeploymentOrchestrator{
|
|
DB: db,
|
|
}
|
|
}
|
|
|
|
// Deploy executes the full deployment workflow
|
|
func (o *DeploymentOrchestrator) Deploy() (*DeploymentReport, error) {
|
|
report := &DeploymentReport{
|
|
StartTime: time.Now(),
|
|
}
|
|
|
|
logger.Info("═══════════════════════════════════════════════════")
|
|
logger.Info("Starting Deployment Process")
|
|
logger.Info("═══════════════════════════════════════════════════")
|
|
|
|
// Step 1: Create backup
|
|
logger.Info("Step 1/4: Creating pre-deployment backup...")
|
|
backupPath, err := o.createBackup()
|
|
if err != nil {
|
|
report.Errors = append(report.Errors, fmt.Sprintf("Backup failed: %v", err))
|
|
return report, fmt.Errorf("backup failed: %w", err)
|
|
}
|
|
report.BackupCreated = true
|
|
report.BackupPath = backupPath
|
|
logger.Info("✓ Backup created: %s", backupPath)
|
|
|
|
// Step 2: Check version compatibility
|
|
logger.Info("Step 2/4: Checking version compatibility...")
|
|
if err := o.checkCompatibility(); err != nil {
|
|
report.Errors = append(report.Errors, fmt.Sprintf("Compatibility check failed: %v", err))
|
|
return report, fmt.Errorf("compatibility check failed: %w", err)
|
|
}
|
|
logger.Info("✓ Version compatibility verified")
|
|
|
|
// Step 3: Apply migrations
|
|
logger.Info("Step 3/4: Applying database migrations...")
|
|
migrationsRun, err := o.applyMigrations()
|
|
if err != nil {
|
|
report.Errors = append(report.Errors, fmt.Sprintf("Migration failed: %v", err))
|
|
logger.Error("Migration failed, attempting rollback...")
|
|
// Rollback would happen here in production
|
|
return report, fmt.Errorf("migration failed: %w", err)
|
|
}
|
|
report.MigrationsRun = migrationsRun
|
|
if migrationsRun > 0 {
|
|
logger.Info("✓ Applied %d migration(s)", migrationsRun)
|
|
} else {
|
|
logger.Info("✓ No migrations to apply")
|
|
}
|
|
|
|
// Step 4: Validate deployment
|
|
logger.Info("Step 4/4: Validating deployment...")
|
|
if err := o.validateDeployment(); err != nil {
|
|
report.Errors = append(report.Errors, fmt.Sprintf("Validation failed: %v", err))
|
|
return report, fmt.Errorf("validation failed: %w", err)
|
|
}
|
|
report.ValidationPassed = true
|
|
logger.Info("✓ Deployment validated successfully")
|
|
|
|
report.Success = true
|
|
report.EndTime = time.Now()
|
|
report.Duration = report.EndTime.Sub(report.StartTime)
|
|
|
|
logger.Info("═══════════════════════════════════════════════════")
|
|
logger.Info("Deployment Completed Successfully")
|
|
logger.Info("Duration: %v", report.Duration)
|
|
logger.Info("═══════════════════════════════════════════════════")
|
|
|
|
return report, nil
|
|
}
|
|
|
|
// createBackup creates a pre-deployment backup
|
|
func (o *DeploymentOrchestrator) createBackup() (string, error) {
|
|
// Get current version for backup metadata
|
|
runner := migrations.NewMigrationRunner(o.DB)
|
|
currentVer, migNum, err := runner.GetCurrentVersion()
|
|
if err != nil {
|
|
currentVer = "unknown"
|
|
migNum = 0
|
|
}
|
|
|
|
// Create backup using backup service
|
|
backupService := backup.NewBackupService()
|
|
metadata, err := backupService.CreateJSONBackup(currentVer, migNum)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create backup: %w", err)
|
|
}
|
|
|
|
return metadata.FilePath, nil
|
|
}
|
|
|
|
// checkCompatibility verifies version compatibility
|
|
func (o *DeploymentOrchestrator) checkCompatibility() error {
|
|
runner := migrations.NewMigrationRunner(o.DB)
|
|
currentVer, _, err := runner.GetCurrentVersion()
|
|
if err != nil {
|
|
// If version table doesn't exist, this might be a fresh install
|
|
currentVer = ""
|
|
}
|
|
|
|
compat := version.CheckCompatibility(currentVer)
|
|
|
|
if !compat.Compatible && !compat.MigrationNeeded {
|
|
return fmt.Errorf("version incompatible: %s", compat.Reason)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// applyMigrations applies pending database migrations
|
|
func (o *DeploymentOrchestrator) applyMigrations() (int, error) {
|
|
runner := migrations.NewMigrationRunner(o.DB)
|
|
runner.Verbose = true
|
|
|
|
// Get pending migrations
|
|
pending, err := runner.GetPendingMigrations()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to get pending migrations: %w", err)
|
|
}
|
|
|
|
if len(pending) == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
// Apply all pending migrations
|
|
results, err := runner.ApplyAll()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to apply migrations: %w", err)
|
|
}
|
|
|
|
// Count successful migrations
|
|
successCount := 0
|
|
for _, result := range results {
|
|
if result.Success {
|
|
successCount++
|
|
}
|
|
}
|
|
|
|
return successCount, nil
|
|
}
|
|
|
|
// validateDeployment validates the database after deployment
|
|
func (o *DeploymentOrchestrator) validateDeployment() error {
|
|
// Check that version was updated
|
|
runner := migrations.NewMigrationRunner(o.DB)
|
|
currentVer, _, err := runner.GetCurrentVersion()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get version after migration: %w", err)
|
|
}
|
|
|
|
// Verify version matches required version
|
|
if currentVer != version.GetRequiredDBVersion() {
|
|
return fmt.Errorf("version mismatch after deployment: expected %s, got %s",
|
|
version.GetRequiredDBVersion(), currentVer)
|
|
}
|
|
|
|
// Basic sanity check: verify we can query the database
|
|
var count int64
|
|
if err := o.DB.Table("schema_version").Count(&count).Error; err != nil {
|
|
return fmt.Errorf("database sanity check failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PrepareDeploymentPackage creates an export of all system and master data
|
|
func (o *DeploymentOrchestrator) PrepareDeploymentPackage(exportDir string) (*DeploymentPackage, error) {
|
|
logger.Info("═══════════════════════════════════════════════════")
|
|
logger.Info("Preparing Deployment Package")
|
|
logger.Info("═══════════════════════════════════════════════════")
|
|
|
|
pkg := &DeploymentPackage{
|
|
Version: config.GetVersion(),
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
// Export full database (all data, all tables)
|
|
logger.Info("Exporting complete database...")
|
|
result, err := transfer.ExportDatabase(exportDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("database export failed: %w", err)
|
|
}
|
|
|
|
pkg.ExportPath = result.FilePath
|
|
pkg.RecordCount = result.RecordCount
|
|
logger.Info("✓ Exported %d records to %s", result.RecordCount, result.Filename)
|
|
|
|
logger.Info("═══════════════════════════════════════════════════")
|
|
logger.Info("Deployment Package Ready")
|
|
logger.Info("File: %s", result.FilePath)
|
|
logger.Info("Records: %d", result.RecordCount)
|
|
logger.Info("═══════════════════════════════════════════════════")
|
|
|
|
return pkg, nil
|
|
}
|
|
|
|
// DeploymentPackage contains information about a deployment package
|
|
type DeploymentPackage struct {
|
|
Version string
|
|
Timestamp time.Time
|
|
ExportPath string
|
|
RecordCount int
|
|
}
|
|
|
|
// FullDeploymentWithImport performs a complete deployment including data import
|
|
func (o *DeploymentOrchestrator) FullDeploymentWithImport(importFilePath string) (*DeploymentReport, error) {
|
|
report := &DeploymentReport{
|
|
StartTime: time.Now(),
|
|
}
|
|
|
|
logger.Info("═══════════════════════════════════════════════════")
|
|
logger.Info("Starting Full Deployment With Data Import")
|
|
logger.Info("═══════════════════════════════════════════════════")
|
|
|
|
// Step 1: Create backup of current state
|
|
logger.Info("Step 1/5: Creating pre-deployment backup...")
|
|
backupPath, err := o.createBackup()
|
|
if err != nil {
|
|
report.Errors = append(report.Errors, fmt.Sprintf("Backup failed: %v", err))
|
|
return report, fmt.Errorf("backup failed: %w", err)
|
|
}
|
|
report.BackupCreated = true
|
|
report.BackupPath = backupPath
|
|
logger.Info("✓ Backup created: %s", backupPath)
|
|
|
|
// Step 2: Export current state (before migration)
|
|
logger.Info("Step 2/5: Exporting current database state...")
|
|
exportDir := "./export_temp"
|
|
exportResult, err := transfer.ExportDatabase(exportDir)
|
|
if err != nil {
|
|
report.Warnings = append(report.Warnings, fmt.Sprintf("Current state export failed: %v", err))
|
|
logger.Warn("Could not export current state: %v", err)
|
|
} else {
|
|
logger.Info("✓ Current state exported: %s", exportResult.Filename)
|
|
}
|
|
|
|
// Step 3: Check version compatibility
|
|
logger.Info("Step 3/5: Checking version compatibility...")
|
|
if err := o.checkCompatibility(); err != nil {
|
|
report.Errors = append(report.Errors, fmt.Sprintf("Compatibility check failed: %v", err))
|
|
return report, fmt.Errorf("compatibility check failed: %w", err)
|
|
}
|
|
logger.Info("✓ Version compatibility verified")
|
|
|
|
// Step 4: Apply migrations
|
|
logger.Info("Step 4/5: Applying database migrations...")
|
|
migrationsRun, err := o.applyMigrations()
|
|
if err != nil {
|
|
report.Errors = append(report.Errors, fmt.Sprintf("Migration failed: %v", err))
|
|
logger.Error("Migration failed! Rollback required.")
|
|
return report, fmt.Errorf("migration failed: %w", err)
|
|
}
|
|
report.MigrationsRun = migrationsRun
|
|
if migrationsRun > 0 {
|
|
logger.Info("✓ Applied %d migration(s)", migrationsRun)
|
|
} else {
|
|
logger.Info("✓ No migrations needed")
|
|
}
|
|
|
|
// Step 5: Import data if provided
|
|
if importFilePath != "" {
|
|
logger.Info("Step 5/5: Importing data from %s...", importFilePath)
|
|
importResult, err := transfer.ImportDatabase(importFilePath)
|
|
if err != nil {
|
|
report.Errors = append(report.Errors, fmt.Sprintf("Data import failed: %v", err))
|
|
return report, fmt.Errorf("data import failed: %w", err)
|
|
}
|
|
logger.Info("✓ Imported %d records", importResult.RecordCount)
|
|
} else {
|
|
logger.Info("Step 5/5: No data import requested")
|
|
}
|
|
|
|
// Validate
|
|
logger.Info("Validating deployment...")
|
|
if err := o.validateDeployment(); err != nil {
|
|
report.Errors = append(report.Errors, fmt.Sprintf("Validation failed: %v", err))
|
|
return report, fmt.Errorf("validation failed: %w", err)
|
|
}
|
|
report.ValidationPassed = true
|
|
logger.Info("✓ Deployment validated successfully")
|
|
|
|
report.Success = true
|
|
report.EndTime = time.Now()
|
|
report.Duration = report.EndTime.Sub(report.StartTime)
|
|
|
|
logger.Info("═══════════════════════════════════════════════════")
|
|
logger.Info("Full Deployment Completed Successfully")
|
|
logger.Info("Duration: %v", report.Duration)
|
|
logger.Info("═══════════════════════════════════════════════════")
|
|
|
|
return report, nil
|
|
}
|