b05858b40d
Introduce ConfigService and /api/v1/config endpoints for system portability. - Export: emits a versioned JSON document covering credentials (encrypted secrets preserved), connections, schedules, rules (with nested condition groups, conditions, and actions), and non-sensitive app settings. - Import: validates the format version and upserts each entity by ID. Supports a dryRun mode that plans the import without writing, and emits per-entity warnings for secrets that need re-entry. - Audit: Export and Import actions emit ConfigChange audit events with entity counts. - Wiring: add ConfigService to server.Dependencies and instantiate it in cli.RunForeground.
220 lines
6.5 KiB
Go
220 lines
6.5 KiB
Go
// Package cli implements command-line interface handlers for OrchestrAD
|
|
package cli
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"syscall"
|
|
|
|
"github.com/Grace-Solutions/OrchestrAD/internal/audit"
|
|
"github.com/Grace-Solutions/OrchestrAD/internal/auth"
|
|
"github.com/Grace-Solutions/OrchestrAD/internal/config"
|
|
"github.com/Grace-Solutions/OrchestrAD/internal/crypto"
|
|
"github.com/Grace-Solutions/OrchestrAD/internal/db"
|
|
"github.com/Grace-Solutions/OrchestrAD/internal/logging"
|
|
"github.com/Grace-Solutions/OrchestrAD/internal/repository"
|
|
"github.com/Grace-Solutions/OrchestrAD/internal/rules/engine"
|
|
"github.com/Grace-Solutions/OrchestrAD/internal/rules/runner"
|
|
"github.com/Grace-Solutions/OrchestrAD/internal/scheduler"
|
|
"github.com/Grace-Solutions/OrchestrAD/internal/server"
|
|
"github.com/Grace-Solutions/OrchestrAD/internal/services"
|
|
)
|
|
|
|
// RunInit initializes the application: validates config, initializes DB, runs migrations
|
|
func RunInit() error {
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return fmt.Errorf("loading config: %w", err)
|
|
}
|
|
|
|
logger := logging.New(cfg.Logging)
|
|
logger.Info("CLI", "Initializing OrchestrAD...")
|
|
|
|
// Initialize database
|
|
database, err := db.New(cfg.Database, logger)
|
|
if err != nil {
|
|
return fmt.Errorf("initializing database: %w", err)
|
|
}
|
|
defer database.Close()
|
|
|
|
// Run migrations
|
|
if err := database.Migrate(); err != nil {
|
|
return fmt.Errorf("running migrations: %w", err)
|
|
}
|
|
|
|
logger.Info("CLI", "Initialization complete")
|
|
return nil
|
|
}
|
|
|
|
// RunForeground runs the application in foreground mode
|
|
func RunForeground() error {
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return fmt.Errorf("loading config: %w", err)
|
|
}
|
|
|
|
logger := logging.New(cfg.Logging)
|
|
logger.Info("CLI", "Starting OrchestrAD in foreground mode...")
|
|
|
|
// Initialize database
|
|
database, err := db.New(cfg.Database, logger)
|
|
if err != nil {
|
|
return fmt.Errorf("initializing database: %w", err)
|
|
}
|
|
defer database.Close()
|
|
|
|
// Run migrations
|
|
if err := database.Migrate(); err != nil {
|
|
return fmt.Errorf("running migrations: %w", err)
|
|
}
|
|
|
|
// Derive a 32-byte AES key from the configured secret key
|
|
keyHash := sha256.Sum256(cfg.SecretKey)
|
|
encryptor, err := crypto.NewEncryptor(keyHash[:])
|
|
if err != nil {
|
|
return fmt.Errorf("initializing encryptor: %w", err)
|
|
}
|
|
|
|
// Build services, repositories, and the execution pipeline
|
|
connService := services.NewConnectionService(database.Conn(), encryptor, logger)
|
|
ruleRunner := runner.New(database.Conn(), connService, logger)
|
|
auditService := audit.NewService(database.Conn())
|
|
deps := server.Dependencies{
|
|
Runner: ruleRunner,
|
|
Engine: engine.NewEngine(logger),
|
|
ConnService: connService,
|
|
AuthService: auth.NewService(database.Conn()),
|
|
RuleService: services.NewRuleService(database.Conn(), logger),
|
|
CredService: services.NewCredentialService(database.Conn(), encryptor, logger),
|
|
AuditService: auditService,
|
|
RuleRepo: repository.NewRuleRepository(database.Conn()),
|
|
ConnRepo: repository.NewConnectionRepository(database.Conn()),
|
|
ScheduleRepo: repository.NewScheduleRepository(database.Conn()),
|
|
RunRepo: repository.NewRuleRunRepository(database.Conn()),
|
|
UserRepo: repository.NewUserRepository(database.Conn()),
|
|
APIKeyService: services.NewAPIKeyService(database.Conn(), logger),
|
|
BackupService: services.NewBackupService(database, filepath.Join(cfg.DataPath, "backups"), 10, logger),
|
|
SettingsService: services.NewSettingsService(database.Conn(), logger),
|
|
DashboardService: services.NewDashboardService(database.Conn(), logger),
|
|
ConfigService: services.NewConfigService(database.Conn(), logger),
|
|
}
|
|
|
|
// Record a service-start audit event so the trail is bootstrapped
|
|
_ = auditService.LogSuccess(audit.EventServiceStart, "System", "Start", map[string]any{
|
|
"mode": "foreground",
|
|
})
|
|
|
|
// Setup graceful shutdown
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Start the scheduler so enabled rules fire automatically
|
|
sched := scheduler.New(database.Conn(), ruleRunner, logger)
|
|
if err := sched.Start(ctx); err != nil {
|
|
return fmt.Errorf("starting scheduler: %w", err)
|
|
}
|
|
defer sched.Stop()
|
|
|
|
// Create HTTP server
|
|
srv := server.New(cfg, database, deps, logger)
|
|
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
go func() {
|
|
<-sigChan
|
|
logger.Info("CLI", "Shutdown signal received, stopping server...")
|
|
cancel()
|
|
}()
|
|
|
|
return srv.Run(ctx)
|
|
}
|
|
|
|
// RunInstall installs the application as a system service
|
|
func RunInstall() error {
|
|
return fmt.Errorf("service install not yet implemented for this platform")
|
|
}
|
|
|
|
// RunUninstall removes the system service
|
|
func RunUninstall() error {
|
|
return fmt.Errorf("service uninstall not yet implemented for this platform")
|
|
}
|
|
|
|
// RunStart starts the installed service
|
|
func RunStart() error {
|
|
return fmt.Errorf("service start not yet implemented for this platform")
|
|
}
|
|
|
|
// RunStop stops the installed service
|
|
func RunStop() error {
|
|
return fmt.Errorf("service stop not yet implemented for this platform")
|
|
}
|
|
|
|
// RunMigrate applies database migrations
|
|
func RunMigrate() error {
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return fmt.Errorf("loading config: %w", err)
|
|
}
|
|
|
|
logger := logging.New(cfg.Logging)
|
|
logger.Info("CLI", "Running database migrations...")
|
|
|
|
database, err := db.New(cfg.Database, logger)
|
|
if err != nil {
|
|
return fmt.Errorf("initializing database: %w", err)
|
|
}
|
|
defer database.Close()
|
|
|
|
if err := database.Migrate(); err != nil {
|
|
return fmt.Errorf("running migrations: %w", err)
|
|
}
|
|
|
|
logger.Info("CLI", "Migrations complete")
|
|
return nil
|
|
}
|
|
|
|
// RunBackup creates a manual database backup
|
|
func RunBackup() error {
|
|
return fmt.Errorf("backup not yet implemented")
|
|
}
|
|
|
|
// RunRestore restores the database from a backup file
|
|
func RunRestore(filepath string) error {
|
|
return fmt.Errorf("restore not yet implemented: %s", filepath)
|
|
}
|
|
|
|
// RunDoctor validates system configuration and health
|
|
func RunDoctor() error {
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return fmt.Errorf("config validation failed: %w", err)
|
|
}
|
|
|
|
logger := logging.New(cfg.Logging)
|
|
logger.Info("Doctor", "Running system health checks...")
|
|
|
|
// Check database
|
|
database, err := db.New(cfg.Database, logger)
|
|
if err != nil {
|
|
logger.Error("Doctor", "Database check failed: %v", err)
|
|
} else {
|
|
logger.Info("Doctor", "Database: OK")
|
|
database.Close()
|
|
}
|
|
|
|
// Check secret key
|
|
if len(cfg.SecretKey) < 32 {
|
|
logger.Warn("Doctor", "Secret key should be at least 32 bytes")
|
|
} else {
|
|
logger.Info("Doctor", "Secret key: OK")
|
|
}
|
|
|
|
logger.Info("Doctor", "Health checks complete")
|
|
return nil
|
|
}
|