mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-12 05:45:27 +00:00
- Added secure config export/import with passphrase-based encryption - CLI commands: pulse config export/import with AES-256-GCM encryption - Auto-import on Docker startup via PULSE_INIT_CONFIG_FILE/DATA env vars - API endpoints /api/config/export and /api/config/import (require API_TOKEN) - Configs remain encrypted throughout export/import process - Perfect for GitOps, CI/CD, and infrastructure as code workflows This allows users to configure Pulse once via UI, export the encrypted config, and deploy it automatically to multiple instances without manual reconfiguration. Addresses #249 - Config management for automation enthusiasts
204 lines
No EOL
5.4 KiB
Go
204 lines
No EOL
5.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/api"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
|
"github.com/rs/zerolog"
|
|
"github.com/rs/zerolog/log"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var rootCmd = &cobra.Command{
|
|
Use: "pulse",
|
|
Short: "Pulse - Proxmox VE and PBS monitoring system",
|
|
Long: `Pulse is a real-time monitoring system for Proxmox Virtual Environment (PVE) and Proxmox Backup Server (PBS)`,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
runServer()
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
// Add config command
|
|
rootCmd.AddCommand(configCmd)
|
|
}
|
|
|
|
func main() {
|
|
if err := rootCmd.Execute(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func runServer() {
|
|
// Initialize logger
|
|
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
|
|
|
// Check for auto-import on first startup
|
|
if shouldAutoImport() {
|
|
if err := performAutoImport(); err != nil {
|
|
log.Error().Err(err).Msg("Auto-import failed, continuing with normal startup")
|
|
}
|
|
}
|
|
|
|
// Load unified configuration
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("Failed to load configuration")
|
|
}
|
|
|
|
log.Info().Msg("Starting Pulse monitoring server")
|
|
|
|
// Create context that cancels on interrupt
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Initialize WebSocket hub first
|
|
wsHub := websocket.NewHub(nil)
|
|
// Set allowed origins from configuration
|
|
if cfg.AllowedOrigins != "" && cfg.AllowedOrigins != "*" {
|
|
wsHub.SetAllowedOrigins(strings.Split(cfg.AllowedOrigins, ","))
|
|
}
|
|
go wsHub.Run()
|
|
|
|
// Initialize reloadable monitoring system
|
|
reloadableMonitor, err := monitoring.NewReloadableMonitor(cfg, wsHub)
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("Failed to initialize monitoring system")
|
|
}
|
|
|
|
// Set state getter for WebSocket hub
|
|
wsHub.SetStateGetter(func() interface{} {
|
|
return reloadableMonitor.GetState()
|
|
})
|
|
|
|
// Start monitoring
|
|
reloadableMonitor.Start(ctx)
|
|
|
|
// Initialize API server with reload function
|
|
reloadFunc := func() error {
|
|
return reloadableMonitor.Reload()
|
|
}
|
|
router := api.NewRouter(cfg, reloadableMonitor.GetMonitor(), wsHub, reloadFunc)
|
|
|
|
// Create HTTP server with unified configuration
|
|
// In production, serve everything (frontend + API) on the frontend port
|
|
srv := &http.Server{
|
|
Addr: fmt.Sprintf("%s:%d", cfg.BackendHost, cfg.FrontendPort),
|
|
Handler: router,
|
|
ReadTimeout: 15 * time.Second,
|
|
WriteTimeout: 15 * time.Second,
|
|
IdleTimeout: 60 * time.Second,
|
|
}
|
|
|
|
// Start server
|
|
go func() {
|
|
log.Info().
|
|
Str("host", cfg.BackendHost).
|
|
Int("port", cfg.FrontendPort).
|
|
Msg("Server listening")
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatal().Err(err).Msg("Failed to start server")
|
|
}
|
|
}()
|
|
|
|
// Wait for interrupt signal
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
|
<-sigChan
|
|
|
|
log.Info().Msg("Shutting down server...")
|
|
|
|
// Graceful shutdown
|
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer shutdownCancel()
|
|
|
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
|
log.Error().Err(err).Msg("Server shutdown error")
|
|
}
|
|
|
|
// Stop monitoring
|
|
cancel()
|
|
reloadableMonitor.Stop()
|
|
|
|
log.Info().Msg("Server stopped")
|
|
}
|
|
|
|
// shouldAutoImport checks if auto-import environment variables are set
|
|
func shouldAutoImport() bool {
|
|
// Check if config already exists
|
|
configPath := os.Getenv("PULSE_DATA_DIR")
|
|
if configPath == "" {
|
|
configPath = "/etc/pulse"
|
|
}
|
|
|
|
// If nodes.enc already exists, skip auto-import
|
|
if _, err := os.Stat(filepath.Join(configPath, "nodes.enc")); err == nil {
|
|
return false
|
|
}
|
|
|
|
// Check for auto-import environment variables
|
|
return os.Getenv("PULSE_INIT_CONFIG_DATA") != "" ||
|
|
os.Getenv("PULSE_INIT_CONFIG_FILE") != ""
|
|
}
|
|
|
|
// performAutoImport imports configuration from environment variables
|
|
func performAutoImport() error {
|
|
configData := os.Getenv("PULSE_INIT_CONFIG_DATA")
|
|
configFile := os.Getenv("PULSE_INIT_CONFIG_FILE")
|
|
configPass := os.Getenv("PULSE_INIT_CONFIG_PASSPHRASE")
|
|
|
|
if configPass == "" {
|
|
return fmt.Errorf("PULSE_INIT_CONFIG_PASSPHRASE is required for auto-import")
|
|
}
|
|
|
|
var encryptedData string
|
|
|
|
// Get data from file or direct data
|
|
if configFile != "" {
|
|
data, err := os.ReadFile(configFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read config file: %w", err)
|
|
}
|
|
encryptedData = string(data)
|
|
} else if configData != "" {
|
|
// Try to decode base64 if it looks encoded
|
|
if decoded, err := base64.StdEncoding.DecodeString(configData); err == nil {
|
|
encryptedData = string(decoded)
|
|
} else {
|
|
encryptedData = configData
|
|
}
|
|
} else {
|
|
return fmt.Errorf("no config data provided")
|
|
}
|
|
|
|
// Load configuration path
|
|
configPath := os.Getenv("PULSE_DATA_DIR")
|
|
if configPath == "" {
|
|
configPath = "/etc/pulse"
|
|
}
|
|
|
|
// Create persistence manager
|
|
persistence := config.NewConfigPersistence(configPath)
|
|
|
|
// Import configuration
|
|
if err := persistence.ImportConfig(encryptedData, configPass); err != nil {
|
|
return fmt.Errorf("failed to import configuration: %w", err)
|
|
}
|
|
|
|
log.Info().Msg("Configuration auto-imported successfully")
|
|
return nil
|
|
} |