mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
Resolves two remaining TODOs from codebase audit.
## 1. PBS/PMG Test Harness Stubs
**Location:** internal/monitoring/harness_integration.go:149-151
**Changes:**
- Added PBS client stub registration: `monitor.pbsClients[inst.Name] = &pbs.Client{}`
- Added PMG client stub registration: `monitor.pmgClients[inst.Name] = &pmg.Client{}`
- Added imports for pkg/pbs and pkg/pmg
**Purpose:**
Enables integration test scenarios to include PBS and PMG instance types
alongside existing PVE support. Stubs allow scheduler to register and
execute tasks for these instance types during integration testing.
**Testing:**
✅ TestAdaptiveSchedulerIntegration passes (55.5s)
✅ Integration test harness now supports all three instance types
## 2. HTTP Config URL Fetch
**Location:** cmd/pulse/config.go:226-261
**Problem:**
`PULSE_INIT_CONFIG_URL` was recognized but not implemented, returning
"URL import not yet implemented" error.
**Implementation:**
- URL validation (http/https schemes only)
- HTTP client with 15 second timeout
- Status code validation (2xx required)
- Empty response detection
- Base64 decoding with fallback to raw data
- Matches existing env-var behavior for `PULSE_INIT_CONFIG_DATA`
**Security:**
- Both HTTP and HTTPS supported (HTTPS recommended for production)
- URL scheme validation prevents file:// or other protocols
- Timeout prevents hanging on unresponsive servers
**Usage:**
```bash
export PULSE_INIT_CONFIG_URL="https://config-server/encrypted-config"
export PULSE_INIT_CONFIG_PASSPHRASE="secret"
pulse config auto-import
```
**Testing:**
✅ Code compiles cleanly
✅ Follows same pattern as existing PULSE_INIT_CONFIG_DATA handling
## Impact
- Completes integration test infrastructure for all instance types
- Enables automated config distribution via HTTP(S) for container deployments
- Removes last TODOs from codebase (no TODO/FIXME remaining in Go files)
305 lines
8.8 KiB
Go
305 lines
8.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
var (
|
|
exportFile string
|
|
importFile string
|
|
passphrase string
|
|
forceImport bool
|
|
)
|
|
|
|
var configCmd = &cobra.Command{
|
|
Use: "config",
|
|
Short: "Configuration management commands",
|
|
Long: `Manage Pulse configuration settings`,
|
|
}
|
|
|
|
var configInfoCmd = &cobra.Command{
|
|
Use: "info",
|
|
Short: "Show configuration information",
|
|
Long: `Display information about Pulse configuration`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
fmt.Println("Pulse Configuration Information")
|
|
fmt.Println("==============================")
|
|
fmt.Println()
|
|
fmt.Println("Configuration is managed through the web UI.")
|
|
fmt.Println("Settings are stored in encrypted files at /etc/pulse/")
|
|
fmt.Println()
|
|
fmt.Println("Configuration files:")
|
|
fmt.Println(" - nodes.enc : Encrypted Proxmox node configurations")
|
|
fmt.Println(" - email.enc : Encrypted email settings")
|
|
fmt.Println(" - system.json : System settings (polling interval, etc)")
|
|
fmt.Println(" - alerts.json : Alert rules and thresholds")
|
|
fmt.Println(" - webhooks.json : Webhook configurations")
|
|
fmt.Println()
|
|
fmt.Println("To configure Pulse, use the Settings tab in the web UI.")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var configExportCmd = &cobra.Command{
|
|
Use: "export",
|
|
Short: "Export configuration with encryption",
|
|
Long: `Export all Pulse configuration to an encrypted file`,
|
|
Example: ` # Export with interactive passphrase prompt
|
|
pulse config export -o pulse-config.enc
|
|
|
|
# Export with passphrase from environment variable
|
|
PULSE_PASSPHRASE=mysecret pulse config export -o pulse-config.enc`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// Get passphrase
|
|
pass := getPassphrase("Enter passphrase for encryption: ", false)
|
|
if pass == "" {
|
|
return fmt.Errorf("passphrase is required")
|
|
}
|
|
|
|
// Load configuration path
|
|
configPath := os.Getenv("PULSE_DATA_DIR")
|
|
if configPath == "" {
|
|
configPath = "/etc/pulse"
|
|
}
|
|
|
|
// Create persistence manager
|
|
persistence := config.NewConfigPersistence(configPath)
|
|
|
|
// Export configuration
|
|
exportedData, err := persistence.ExportConfig(pass)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to export configuration: %w", err)
|
|
}
|
|
|
|
// Write to file or stdout
|
|
if exportFile != "" {
|
|
if err := ioutil.WriteFile(exportFile, []byte(exportedData), 0600); err != nil {
|
|
return fmt.Errorf("failed to write export file: %w", err)
|
|
}
|
|
fmt.Printf("Configuration exported to %s\n", exportFile)
|
|
} else {
|
|
fmt.Println(exportedData)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var configImportCmd = &cobra.Command{
|
|
Use: "import",
|
|
Short: "Import configuration from encrypted export",
|
|
Long: `Import Pulse configuration from an encrypted export file`,
|
|
Example: ` # Import with interactive passphrase prompt
|
|
pulse config import -i pulse-config.enc
|
|
|
|
# Import with passphrase from environment variable
|
|
PULSE_PASSPHRASE=mysecret pulse config import -i pulse-config.enc
|
|
|
|
# Force import without confirmation
|
|
pulse config import -i pulse-config.enc --force`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// Check if import file is specified
|
|
if importFile == "" {
|
|
return fmt.Errorf("import file is required (use -i flag)")
|
|
}
|
|
|
|
// Read import file
|
|
data, err := ioutil.ReadFile(importFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read import file: %w", err)
|
|
}
|
|
|
|
// Get passphrase
|
|
pass := getPassphrase("Enter passphrase for decryption: ", false)
|
|
if pass == "" {
|
|
return fmt.Errorf("passphrase is required")
|
|
}
|
|
|
|
// Confirm import unless forced
|
|
if !forceImport {
|
|
fmt.Println("WARNING: This will overwrite all existing configuration!")
|
|
fmt.Print("Continue? (yes/no): ")
|
|
reader := bufio.NewReader(os.Stdin)
|
|
response, _ := reader.ReadString('\n')
|
|
response = strings.TrimSpace(strings.ToLower(response))
|
|
if response != "yes" && response != "y" {
|
|
fmt.Println("Import cancelled")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// 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(string(data), pass); err != nil {
|
|
return fmt.Errorf("failed to import configuration: %w", err)
|
|
}
|
|
|
|
fmt.Println("Configuration imported successfully")
|
|
fmt.Println("Please restart Pulse for changes to take effect:")
|
|
fmt.Println(" sudo systemctl restart pulse")
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// getPassphrase prompts for a passphrase or gets it from environment
|
|
func getPassphrase(prompt string, confirm bool) string {
|
|
// Check environment variable first
|
|
if pass := os.Getenv("PULSE_PASSPHRASE"); pass != "" {
|
|
return pass
|
|
}
|
|
|
|
// Check if passphrase flag was set
|
|
if passphrase != "" {
|
|
return passphrase
|
|
}
|
|
|
|
// Interactive prompt
|
|
fmt.Print(prompt)
|
|
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
|
|
fmt.Println()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
pass := string(bytePassword)
|
|
|
|
// Confirm if requested
|
|
if confirm {
|
|
fmt.Print("Confirm passphrase: ")
|
|
bytePassword2, err := term.ReadPassword(int(syscall.Stdin))
|
|
fmt.Println()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
if string(bytePassword2) != pass {
|
|
fmt.Println("Passphrases do not match")
|
|
return ""
|
|
}
|
|
}
|
|
|
|
return pass
|
|
}
|
|
|
|
// Environment variable support for initial setup
|
|
var configAutoImportCmd = &cobra.Command{
|
|
Use: "auto-import",
|
|
Hidden: true, // Hidden command for automated setup
|
|
Short: "Auto-import configuration on startup",
|
|
Long: `Automatically import configuration from URL or file on first startup`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// Check for auto-import environment variables
|
|
configURL := os.Getenv("PULSE_INIT_CONFIG_URL")
|
|
configData := os.Getenv("PULSE_INIT_CONFIG_DATA")
|
|
configPass := os.Getenv("PULSE_INIT_CONFIG_PASSPHRASE")
|
|
|
|
if configURL == "" && configData == "" {
|
|
return nil // Nothing to import
|
|
}
|
|
|
|
if configPass == "" {
|
|
return fmt.Errorf("PULSE_INIT_CONFIG_PASSPHRASE is required for auto-import")
|
|
}
|
|
|
|
var encryptedData string
|
|
|
|
// Get data from URL or direct data
|
|
if configURL != "" {
|
|
parsedURL, err := url.Parse(configURL)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid PULSE_INIT_CONFIG_URL: %w", err)
|
|
}
|
|
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
|
return fmt.Errorf("unsupported URL scheme %q for PULSE_INIT_CONFIG_URL", parsedURL.Scheme)
|
|
}
|
|
|
|
client := &http.Client{Timeout: 15 * time.Second}
|
|
resp, err := client.Get(configURL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to fetch configuration from URL: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return fmt.Errorf("failed to fetch configuration from URL: %s", resp.Status)
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read configuration response: %w", err)
|
|
}
|
|
if len(body) == 0 {
|
|
return fmt.Errorf("configuration response from URL was empty")
|
|
}
|
|
|
|
trimmed := strings.TrimSpace(string(body))
|
|
if decoded, err := base64.StdEncoding.DecodeString(trimmed); err == nil {
|
|
encryptedData = string(decoded)
|
|
} else {
|
|
encryptedData = string(body)
|
|
}
|
|
} else if configData != "" {
|
|
// Decode base64 if needed
|
|
if decoded, err := base64.StdEncoding.DecodeString(configData); err == nil {
|
|
encryptedData = string(decoded)
|
|
} else {
|
|
encryptedData = configData
|
|
}
|
|
}
|
|
|
|
// 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 auto-import configuration: %w", err)
|
|
}
|
|
|
|
fmt.Println("Configuration auto-imported successfully")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
configCmd.AddCommand(configInfoCmd)
|
|
configCmd.AddCommand(configExportCmd)
|
|
configCmd.AddCommand(configImportCmd)
|
|
configCmd.AddCommand(configAutoImportCmd)
|
|
|
|
// Export flags
|
|
configExportCmd.Flags().StringVarP(&exportFile, "output", "o", "", "Output file for encrypted configuration")
|
|
configExportCmd.Flags().StringVarP(&passphrase, "passphrase", "p", "", "Passphrase for encryption (or use PULSE_PASSPHRASE env var)")
|
|
|
|
// Import flags
|
|
configImportCmd.Flags().StringVarP(&importFile, "input", "i", "", "Input file with encrypted configuration")
|
|
configImportCmd.Flags().StringVarP(&passphrase, "passphrase", "p", "", "Passphrase for decryption (or use PULSE_PASSPHRASE env var)")
|
|
configImportCmd.Flags().BoolVarP(&forceImport, "force", "f", false, "Force import without confirmation")
|
|
}
|