check again if all goals are met

This commit is contained in:
2026-01-17 00:15:10 +01:00
parent 33f80be1ca
commit 7731aa4125
3 changed files with 733 additions and 0 deletions
+181
View File
@@ -3,7 +3,9 @@ package main
import (
"bamort/config"
"bamort/database"
"bamort/deployment"
"bamort/deployment/migrations"
"bamort/deployment/validator"
"bamort/deployment/version"
"fmt"
"os"
@@ -32,6 +34,12 @@ func main() {
cmdVersion()
case "status":
cmdStatus()
case "prepare":
cmdPrepare()
case "deploy":
cmdDeploy()
case "validate":
cmdValidate()
case "help", "--help", "-h":
printHelp()
default:
@@ -46,9 +54,16 @@ func printHelp() {
fmt.Printf("Version: %s\n\n", config.GetVersion())
fmt.Println("Usage: deploy <command> [options]")
fmt.Println("\nCommands:")
fmt.Printf(" %sprepare%s Create deployment package (export all data)\n", ColorGreen, ColorReset)
fmt.Printf(" %sdeploy%s Run full deployment (backup → migrate → validate)\n", ColorGreen, ColorReset)
fmt.Printf(" %svalidate%s Validate database schema and data integrity\n", ColorGreen, ColorReset)
fmt.Printf(" %sstatus%s Show current database version and pending migrations\n", ColorGreen, ColorReset)
fmt.Printf(" %sversion%s Show version information\n", ColorGreen, ColorReset)
fmt.Printf(" %shelp%s Show this help message\n", ColorGreen, ColorReset)
fmt.Println("\nExamples:")
fmt.Println(" deploy prepare # Create deployment package in ./export_temp")
fmt.Println(" deploy deploy # Run full deployment workflow")
fmt.Println(" deploy validate # Validate database schema")
fmt.Println()
}
@@ -125,3 +140,169 @@ func printError(format string, args ...interface{}) {
func printWarning(format string, args ...interface{}) {
fmt.Printf("%s⚠ "+format+"%s\n", append([]interface{}{ColorYellow}, append(args, ColorReset)...)...)
}
func printSuccess(format string, args ...interface{}) {
fmt.Printf("%s✓ "+format+"%s\n", append([]interface{}{ColorGreen}, append(args, ColorReset)...)...)
}
// cmdPrepare creates a deployment package with full database export
func cmdPrepare() {
printBanner("Prepare Deployment Package")
// Connect to database
database.DB = database.ConnectDatabase()
if database.DB == nil {
printError("Failed to connect to database")
os.Exit(1)
}
orchestrator := deployment.NewOrchestrator(database.DB)
exportDir := "./export_temp"
if len(os.Args) > 2 {
exportDir = os.Args[2]
}
fmt.Printf("\nExporting to: %s%s%s\n", ColorCyan, exportDir, ColorReset)
fmt.Println("This will create a complete backup of all system and master data...")
fmt.Println()
pkg, err := orchestrator.PrepareDeploymentPackage(exportDir)
if err != nil {
printError("Failed to prepare deployment package: %v", err)
os.Exit(1)
}
fmt.Println()
printSuccess("Deployment package created successfully!")
fmt.Printf("\n%sPackage Details:%s\n", ColorBold, ColorReset)
fmt.Printf(" Version: %s\n", pkg.Version)
fmt.Printf(" Export File: %s\n", pkg.ExportPath)
fmt.Printf(" Records: %d\n", pkg.RecordCount)
fmt.Printf(" Timestamp: %s\n", pkg.Timestamp.Format("2006-01-02 15:04:05"))
fmt.Println()
fmt.Println("This export can be imported on the target system after migration.")
fmt.Println()
}
// cmdDeploy runs the full deployment workflow
func cmdDeploy() {
printBanner("Full Deployment")
// Connect to database
database.DB = database.ConnectDatabase()
if database.DB == nil {
printError("Failed to connect to database")
os.Exit(1)
}
orchestrator := deployment.NewOrchestrator(database.DB)
fmt.Println("\nThis will:")
fmt.Println(" 1. Create a backup of the current database")
fmt.Println(" 2. Check version compatibility")
fmt.Println(" 3. Apply pending migrations")
fmt.Println(" 4. Validate the deployment")
fmt.Println()
fmt.Printf("%sWARNING:%s This operation will modify the database!\n", ColorYellow, ColorReset)
fmt.Print("Continue? (yes/no): ")
var confirm string
fmt.Scanln(&confirm)
if confirm != "yes" && confirm != "y" {
fmt.Println("Deployment cancelled.")
os.Exit(0)
}
fmt.Println()
report, err := orchestrator.Deploy()
if err != nil {
printError("Deployment failed: %v", err)
fmt.Println()
if report.BackupCreated {
fmt.Printf("Backup available at: %s\n", report.BackupPath)
}
if len(report.Errors) > 0 {
fmt.Printf("\n%sErrors:%s\n", ColorRed, ColorReset)
for _, e := range report.Errors {
fmt.Printf(" • %s\n", e)
}
}
os.Exit(1)
}
fmt.Println()
printSuccess("Deployment completed successfully!")
fmt.Printf("\n%sDeployment Summary:%s\n", ColorBold, ColorReset)
fmt.Printf(" Backup: %s\n", report.BackupPath)
fmt.Printf(" Migrations: %d applied\n", report.MigrationsRun)
fmt.Printf(" Duration: %v\n", report.Duration)
fmt.Printf(" Validated: %s✓%s\n", ColorGreen, ColorReset)
fmt.Println()
}
// cmdValidate validates the database schema and data
func cmdValidate() {
printBanner("Database Validation")
// Connect to database
database.DB = database.ConnectDatabase()
if database.DB == nil {
printError("Failed to connect to database")
os.Exit(1)
}
v := validator.NewValidator(database.DB)
fmt.Println("\nValidating database schema and data integrity...")
fmt.Println()
report, err := v.Validate()
if err != nil {
printError("Validation failed: %v", err)
os.Exit(1)
}
fmt.Printf("\n%sValidation Results:%s\n", ColorBold, ColorReset)
fmt.Printf(" Tables Checked: %d\n", report.TablesChecked)
fmt.Printf(" Tables Valid: %d\n", report.TablesValid)
if len(report.Errors) > 0 {
fmt.Printf("\n%sErrors (%d):%s\n", ColorRed, len(report.Errors), ColorReset)
for _, e := range report.Errors {
fmt.Printf(" %s✗%s %s\n", ColorRed, ColorReset, e)
}
}
if len(report.Warnings) > 0 {
fmt.Printf("\n%sWarnings (%d):%s\n", ColorYellow, len(report.Warnings), ColorReset)
for _, w := range report.Warnings {
fmt.Printf(" %s⚠%s %s\n", ColorYellow, ColorReset, w)
}
}
if len(report.MissingTables) > 0 {
fmt.Printf("\n%sMissing Tables:%s\n", ColorRed, ColorReset)
for _, t := range report.MissingTables {
fmt.Printf(" • %s\n", t)
}
}
if len(report.MissingColumns) > 0 {
fmt.Printf("\n%sMissing Columns:%s\n", ColorRed, ColorReset)
for table, cols := range report.MissingColumns {
fmt.Printf(" %s: %v\n", table, cols)
}
}
fmt.Println()
if report.Success {
printSuccess("Validation passed!")
} else {
printError("Validation failed with %d error(s)", len(report.Errors))
os.Exit(1)
}
fmt.Println()
}
+328
View File
@@ -0,0 +1,328 @@
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
}
+224
View File
@@ -0,0 +1,224 @@
package validator
import (
"bamort/logger"
"fmt"
"gorm.io/gorm"
)
// SchemaValidator validates database schema integrity
type SchemaValidator struct {
DB *gorm.DB
}
// ValidationReport contains validation results
type ValidationReport struct {
Success bool
TablesChecked int
TablesValid int
Errors []string
Warnings []string
MissingTables []string
MissingColumns map[string][]string
}
// NewValidator creates a new schema validator
func NewValidator(db *gorm.DB) *SchemaValidator {
return &SchemaValidator{
DB: db,
}
}
// Validate performs comprehensive schema validation
func (v *SchemaValidator) Validate() (*ValidationReport, error) {
report := &ValidationReport{
Success: true,
MissingColumns: make(map[string][]string),
}
logger.Info("Starting database schema validation...")
// Check ALL tables must exist for the application to work properly
// If char_*, equi_*, audit_* tables are missing, /api/maintenance/setupcheck must be called
// So it's best to ensure all tables are present
criticalTables := []string{
// System tables
"schema_version",
"migration_history",
"users",
// Audit tables
"audit_log_entries",
// Character tables
"char_bennies",
"char_characteristics",
"char_char_creation_session",
"char_chars",
"char_eigenschaften",
"char_endurances",
"char_experiances",
"char_health",
"char_motionranges",
"char_skills",
"char_spells",
"char_wealth",
"char_weaponskills",
// Equipment tables
"equi_containers",
"equi_equipments",
"equi_weapons",
// GSM Master Data tables
"gsm_believes",
"gsm_cc_class_category_points",
"gsm_cc_class_spell_points",
"gsm_cc_class_typical_skills",
"gsm_cc_class_typical_spells",
"gsm_character_classes",
"gsm_containers",
"gsm_equipments",
"gsm_lit_sources",
"gsm_misc",
"gsm_skills",
"gsm_spells",
"gsm_transportations",
"gsm_weapons",
"gsm_weaponskills",
// Learning system tables
"learning_class_category_ep_costs",
"learning_class_spell_school_ep_costs",
"learning_skill_categories",
"learning_skill_category_difficulties",
"learning_skill_difficulties",
"learning_skill_improvement_costs",
"learning_spell_level_le_costs",
"learning_spell_schools",
"learning_weaponskill_category_difficulties",
}
for _, table := range criticalTables {
report.TablesChecked++
if v.tableExists(table) {
report.TablesValid++
logger.Debug("✓ Table exists: %s", table)
} else {
report.MissingTables = append(report.MissingTables, table)
report.Errors = append(report.Errors, fmt.Sprintf("Missing table: %s", table))
report.Success = false
logger.Error("✗ Missing table: %s", table)
}
}
// Check schema_version table structure
if v.tableExists("schema_version") {
requiredColumns := []string{"id", "version", "migration_number", "applied_at"}
missingCols := v.checkTableColumns("schema_version", requiredColumns)
if len(missingCols) > 0 {
report.MissingColumns["schema_version"] = missingCols
report.Errors = append(report.Errors,
fmt.Sprintf("schema_version missing columns: %v", missingCols))
report.Success = false
}
}
// Check migration_history table structure
if v.tableExists("migration_history") {
requiredColumns := []string{"id", "migration_number", "description", "applied_at"}
missingCols := v.checkTableColumns("migration_history", requiredColumns)
if len(missingCols) > 0 {
report.MissingColumns["migration_history"] = missingCols
report.Errors = append(report.Errors,
fmt.Sprintf("migration_history missing columns: %v", missingCols))
report.Success = false
}
}
// Validate record counts are reasonable
if err := v.validateDataIntegrity(report); err != nil {
report.Warnings = append(report.Warnings, fmt.Sprintf("Data integrity check: %v", err))
}
if report.Success {
logger.Info("✓ Schema validation passed")
} else {
logger.Error("✗ Schema validation failed with %d error(s)", len(report.Errors))
}
return report, nil
}
// tableExists checks if a table exists in the database
func (v *SchemaValidator) tableExists(tableName string) bool {
return v.DB.Migrator().HasTable(tableName)
}
// checkTableColumns verifies that required columns exist in a table
func (v *SchemaValidator) checkTableColumns(tableName string, requiredColumns []string) []string {
var missing []string
for _, col := range requiredColumns {
if !v.DB.Migrator().HasColumn(tableName, col) {
missing = append(missing, col)
}
}
return missing
}
// validateDataIntegrity performs basic sanity checks on data
func (v *SchemaValidator) validateDataIntegrity(report *ValidationReport) error {
// Check that schema_version has at least one entry
if v.tableExists("schema_version") {
var count int64
if err := v.DB.Table("schema_version").Count(&count).Error; err != nil {
return fmt.Errorf("failed to count schema_version records: %w", err)
}
if count == 0 {
report.Warnings = append(report.Warnings, "schema_version table is empty")
}
}
// Check for orphaned records (basic check)
if v.tableExists("chars") && v.tableExists("users") {
var orphanedChars int64
if err := v.DB.Raw(`
SELECT COUNT(*) FROM chars
WHERE user_id NOT IN (SELECT id FROM users)
`).Scan(&orphanedChars).Error; err == nil {
if orphanedChars > 0 {
report.Warnings = append(report.Warnings,
fmt.Sprintf("Found %d orphaned characters (invalid user_id)", orphanedChars))
}
}
}
return nil
}
// ValidatePostMigration performs post-migration validation
func (v *SchemaValidator) ValidatePostMigration() error {
logger.Info("Performing post-migration validation...")
report, err := v.Validate()
if err != nil {
return fmt.Errorf("validation failed: %w", err)
}
if !report.Success {
return fmt.Errorf("validation found %d error(s): %v", len(report.Errors), report.Errors)
}
if len(report.Warnings) > 0 {
logger.Warn("Validation passed with %d warning(s):", len(report.Warnings))
for _, w := range report.Warnings {
logger.Warn(" - %s", w)
}
}
logger.Info("✓ Post-migration validation successful")
return nil
}