- Replace all emoji characters with ASCII equivalents throughout codebase - Replace Unicode box-drawing characters (═║╔╗╚╝━─) with ASCII (+|-=) - Replace checkmarks (✓✗) with [OK]/[FAIL] markers - 59 files updated, 741 lines changed - Improves terminal compatibility and reduces visual noise
248 lines
8.0 KiB
Go
248 lines
8.0 KiB
Go
// Package drill provides Disaster Recovery drill functionality
|
|
// for testing backup restorability in isolated environments
|
|
package drill
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// DrillConfig holds configuration for a DR drill
|
|
type DrillConfig struct {
|
|
// Backup configuration
|
|
BackupPath string `json:"backup_path"`
|
|
DatabaseName string `json:"database_name"`
|
|
DatabaseType string `json:"database_type"` // postgresql, mysql, mariadb
|
|
|
|
// Docker configuration
|
|
ContainerImage string `json:"container_image"` // e.g., "postgres:15"
|
|
ContainerName string `json:"container_name"` // Generated if empty
|
|
ContainerPort int `json:"container_port"` // Host port mapping
|
|
ContainerTimeout int `json:"container_timeout"` // Startup timeout in seconds
|
|
CleanupOnExit bool `json:"cleanup_on_exit"` // Remove container after drill
|
|
KeepOnFailure bool `json:"keep_on_failure"` // Keep container if drill fails
|
|
|
|
// Validation configuration
|
|
ValidationQueries []ValidationQuery `json:"validation_queries"`
|
|
MinRowCount int64 `json:"min_row_count"` // Minimum rows expected
|
|
ExpectedTables []string `json:"expected_tables"` // Tables that must exist
|
|
CustomChecks []CustomCheck `json:"custom_checks"`
|
|
|
|
// Encryption (if backup is encrypted)
|
|
EncryptionKeyFile string `json:"encryption_key_file,omitempty"`
|
|
EncryptionKeyEnv string `json:"encryption_key_env,omitempty"`
|
|
|
|
// Performance thresholds
|
|
MaxRestoreSeconds int `json:"max_restore_seconds"` // RTO threshold
|
|
MaxQuerySeconds int `json:"max_query_seconds"` // Query timeout
|
|
|
|
// Output
|
|
OutputDir string `json:"output_dir"` // Directory for drill reports
|
|
ReportFormat string `json:"report_format"` // json, markdown, html
|
|
Verbose bool `json:"verbose"`
|
|
}
|
|
|
|
// ValidationQuery represents a SQL query to validate restored data
|
|
type ValidationQuery struct {
|
|
Name string `json:"name"` // Human-readable name
|
|
Query string `json:"query"` // SQL query
|
|
ExpectedValue string `json:"expected_value"` // Expected result (optional)
|
|
MinValue int64 `json:"min_value"` // Minimum expected value
|
|
MaxValue int64 `json:"max_value"` // Maximum expected value
|
|
MustSucceed bool `json:"must_succeed"` // Fail drill if query fails
|
|
}
|
|
|
|
// CustomCheck represents a custom validation check
|
|
type CustomCheck struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"` // row_count, table_exists, column_check
|
|
Table string `json:"table"`
|
|
Column string `json:"column,omitempty"`
|
|
Condition string `json:"condition,omitempty"` // SQL condition
|
|
MinValue int64 `json:"min_value,omitempty"`
|
|
MustSucceed bool `json:"must_succeed"`
|
|
}
|
|
|
|
// DrillResult contains the complete result of a DR drill
|
|
type DrillResult struct {
|
|
// Identification
|
|
DrillID string `json:"drill_id"`
|
|
StartTime time.Time `json:"start_time"`
|
|
EndTime time.Time `json:"end_time"`
|
|
Duration float64 `json:"duration_seconds"`
|
|
|
|
// Configuration
|
|
BackupPath string `json:"backup_path"`
|
|
DatabaseName string `json:"database_name"`
|
|
DatabaseType string `json:"database_type"`
|
|
|
|
// Overall status
|
|
Success bool `json:"success"`
|
|
Status DrillStatus `json:"status"`
|
|
Message string `json:"message"`
|
|
|
|
// Phase timings
|
|
Phases []DrillPhase `json:"phases"`
|
|
|
|
// Validation results
|
|
ValidationResults []ValidationResult `json:"validation_results"`
|
|
CheckResults []CheckResult `json:"check_results"`
|
|
|
|
// Database metrics
|
|
TableCount int `json:"table_count"`
|
|
TotalRows int64 `json:"total_rows"`
|
|
DatabaseSize int64 `json:"database_size_bytes"`
|
|
|
|
// Performance metrics
|
|
RestoreTime float64 `json:"restore_time_seconds"`
|
|
ValidationTime float64 `json:"validation_time_seconds"`
|
|
QueryTimeAvg float64 `json:"query_time_avg_ms"`
|
|
|
|
// RTO/RPO metrics
|
|
ActualRTO float64 `json:"actual_rto_seconds"` // Total time to usable database
|
|
TargetRTO float64 `json:"target_rto_seconds"`
|
|
RTOMet bool `json:"rto_met"`
|
|
|
|
// Container info
|
|
ContainerID string `json:"container_id,omitempty"`
|
|
ContainerKept bool `json:"container_kept"`
|
|
|
|
// Errors and warnings
|
|
Errors []string `json:"errors,omitempty"`
|
|
Warnings []string `json:"warnings,omitempty"`
|
|
}
|
|
|
|
// DrillStatus represents the current status of a drill
|
|
type DrillStatus string
|
|
|
|
const (
|
|
StatusPending DrillStatus = "pending"
|
|
StatusRunning DrillStatus = "running"
|
|
StatusCompleted DrillStatus = "completed"
|
|
StatusFailed DrillStatus = "failed"
|
|
StatusAborted DrillStatus = "aborted"
|
|
StatusPartial DrillStatus = "partial" // Some validations failed
|
|
)
|
|
|
|
// DrillPhase represents a phase in the drill process
|
|
type DrillPhase struct {
|
|
Name string `json:"name"`
|
|
Status string `json:"status"` // pending, running, completed, failed, skipped
|
|
StartTime time.Time `json:"start_time"`
|
|
EndTime time.Time `json:"end_time"`
|
|
Duration float64 `json:"duration_seconds"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
// ValidationResult holds the result of a validation query
|
|
type ValidationResult struct {
|
|
Name string `json:"name"`
|
|
Query string `json:"query"`
|
|
Success bool `json:"success"`
|
|
Result string `json:"result,omitempty"`
|
|
Expected string `json:"expected,omitempty"`
|
|
Duration float64 `json:"duration_ms"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// CheckResult holds the result of a custom check
|
|
type CheckResult struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Success bool `json:"success"`
|
|
Actual int64 `json:"actual,omitempty"`
|
|
Expected int64 `json:"expected,omitempty"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// DefaultConfig returns a DrillConfig with sensible defaults
|
|
func DefaultConfig() *DrillConfig {
|
|
return &DrillConfig{
|
|
ContainerTimeout: 60,
|
|
CleanupOnExit: true,
|
|
KeepOnFailure: true,
|
|
MaxRestoreSeconds: 300, // 5 minutes
|
|
MaxQuerySeconds: 30,
|
|
ReportFormat: "json",
|
|
Verbose: false,
|
|
ValidationQueries: []ValidationQuery{},
|
|
ExpectedTables: []string{},
|
|
CustomChecks: []CustomCheck{},
|
|
}
|
|
}
|
|
|
|
// NewDrillID generates a unique drill ID
|
|
func NewDrillID() string {
|
|
return fmt.Sprintf("drill_%s", time.Now().Format("20060102_150405"))
|
|
}
|
|
|
|
// SaveResult saves the drill result to a file
|
|
func (r *DrillResult) SaveResult(outputDir string) error {
|
|
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create output directory: %w", err)
|
|
}
|
|
|
|
filename := fmt.Sprintf("%s_report.json", r.DrillID)
|
|
filepath := filepath.Join(outputDir, filename)
|
|
|
|
data, err := json.MarshalIndent(r, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal result: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(filepath, data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write result file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadResult loads a drill result from a file
|
|
func LoadResult(filepath string) (*DrillResult, error) {
|
|
data, err := os.ReadFile(filepath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read result file: %w", err)
|
|
}
|
|
|
|
var result DrillResult
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, fmt.Errorf("failed to parse result: %w", err)
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
// IsSuccess returns true if the drill was successful
|
|
func (r *DrillResult) IsSuccess() bool {
|
|
return r.Success && r.Status == StatusCompleted
|
|
}
|
|
|
|
// Summary returns a human-readable summary of the drill
|
|
func (r *DrillResult) Summary() string {
|
|
status := "[OK] PASSED"
|
|
if !r.Success {
|
|
status = "[FAIL] FAILED"
|
|
} else if r.Status == StatusPartial {
|
|
status = "[WARN] PARTIAL"
|
|
}
|
|
|
|
return fmt.Sprintf("%s - %s (%.2fs) - %d tables, %d rows",
|
|
status, r.DatabaseName, r.Duration, r.TableCount, r.TotalRows)
|
|
}
|
|
|
|
// Drill is the interface for DR drill operations
|
|
type Drill interface {
|
|
// Run executes the full DR drill
|
|
Run(ctx context.Context, config *DrillConfig) (*DrillResult, error)
|
|
|
|
// Validate runs validation queries against an existing database
|
|
Validate(ctx context.Context, config *DrillConfig) ([]ValidationResult, error)
|
|
|
|
// Cleanup removes drill resources (containers, temp files)
|
|
Cleanup(ctx context.Context, drillID string) error
|
|
}
|