feat: enhance runtime configuration and system settings management
Improves configuration handling and system settings APIs to support v4.24.0 features including runtime logging controls, adaptive polling configuration, and enhanced config export/persistence. Changes: - Add config override system for discovery service - Enhance system settings API with runtime logging controls - Improve config persistence and export functionality - Update security setup handling - Refine monitoring and discovery service integration These changes provide the backend support for the configuration features documented in the v4.24.0 release.
This commit is contained in:
parent
c91b7874ac
commit
5ebb32ce10
10 changed files with 677 additions and 101 deletions
|
|
@ -28,13 +28,16 @@ import (
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/tempproxy"
|
"github.com/rcourtman/pulse-go-rewrite/internal/tempproxy"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/pkg/discovery"
|
discoveryinternal "github.com/rcourtman/pulse-go-rewrite/internal/discovery"
|
||||||
|
pkgdiscovery "github.com/rcourtman/pulse-go-rewrite/pkg/discovery"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/pkg/pbs"
|
"github.com/rcourtman/pulse-go-rewrite/pkg/pbs"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/pkg/pmg"
|
"github.com/rcourtman/pulse-go-rewrite/pkg/pmg"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
|
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const minProxyReadyVersion = "4.24.0"
|
||||||
|
|
||||||
// SetupCode represents a one-time setup code for secure node registration
|
// SetupCode represents a one-time setup code for secure node registration
|
||||||
type SetupCode struct {
|
type SetupCode struct {
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
|
|
@ -2455,29 +2458,28 @@ func (h *ConfigHandlers) HandleGetSystemSettings(w http.ResponseWriter, r *http.
|
||||||
persistedSettings, err := h.persistence.LoadSystemSettings()
|
persistedSettings, err := h.persistence.LoadSystemSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("Failed to load persisted system settings")
|
log.Warn().Err(err).Msg("Failed to load persisted system settings")
|
||||||
persistedSettings = &config.SystemSettings{}
|
persistedSettings = config.DefaultSystemSettings()
|
||||||
|
}
|
||||||
|
if persistedSettings == nil {
|
||||||
|
persistedSettings = config.DefaultSystemSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current values from running config
|
// Get current values from running config
|
||||||
settings := config.SystemSettings{
|
settings := *persistedSettings
|
||||||
// Note: PVE polling is hardcoded to 10s
|
settings.PBSPollingInterval = int(h.config.PBSPollingInterval.Seconds())
|
||||||
PBSPollingInterval: int(h.config.PBSPollingInterval.Seconds()),
|
settings.BackupPollingInterval = int(h.config.BackupPollingInterval.Seconds())
|
||||||
BackupPollingInterval: int(h.config.BackupPollingInterval.Seconds()),
|
settings.BackendPort = h.config.BackendPort
|
||||||
BackendPort: h.config.BackendPort,
|
settings.FrontendPort = h.config.FrontendPort
|
||||||
FrontendPort: h.config.FrontendPort,
|
settings.AllowedOrigins = h.config.AllowedOrigins
|
||||||
AllowedOrigins: h.config.AllowedOrigins,
|
settings.ConnectionTimeout = int(h.config.ConnectionTimeout.Seconds())
|
||||||
ConnectionTimeout: int(h.config.ConnectionTimeout.Seconds()),
|
settings.UpdateChannel = h.config.UpdateChannel
|
||||||
UpdateChannel: h.config.UpdateChannel,
|
settings.AutoUpdateEnabled = h.config.AutoUpdateEnabled
|
||||||
AutoUpdateEnabled: h.config.AutoUpdateEnabled,
|
settings.AutoUpdateCheckInterval = int(h.config.AutoUpdateCheckInterval.Hours())
|
||||||
AutoUpdateCheckInterval: int(h.config.AutoUpdateCheckInterval.Hours()),
|
settings.AutoUpdateTime = h.config.AutoUpdateTime
|
||||||
AutoUpdateTime: h.config.AutoUpdateTime,
|
settings.LogLevel = h.config.LogLevel
|
||||||
LogLevel: h.config.LogLevel, // Include log level
|
settings.DiscoveryEnabled = h.config.DiscoveryEnabled
|
||||||
Theme: persistedSettings.Theme, // Include theme from persisted settings
|
settings.DiscoverySubnet = h.config.DiscoverySubnet
|
||||||
AllowEmbedding: persistedSettings.AllowEmbedding, // Include allowEmbedding from persisted settings
|
settings.DiscoveryConfig = config.CloneDiscoveryConfig(h.config.Discovery)
|
||||||
AllowedEmbedOrigins: persistedSettings.AllowedEmbedOrigins, // Include allowedEmbedOrigins from persisted settings
|
|
||||||
DiscoveryEnabled: persistedSettings.DiscoveryEnabled, // Include discoveryEnabled from persisted settings
|
|
||||||
DiscoverySubnet: persistedSettings.DiscoverySubnet, // Include discoverySubnet from persisted settings
|
|
||||||
}
|
|
||||||
backupEnabled := h.config.EnableBackupPolling
|
backupEnabled := h.config.EnableBackupPolling
|
||||||
settings.BackupPollingEnabled = &backupEnabled
|
settings.BackupPollingEnabled = &backupEnabled
|
||||||
|
|
||||||
|
|
@ -2927,7 +2929,11 @@ func (h *ConfigHandlers) HandleDiscoverServers(w http.ResponseWriter, r *http.Re
|
||||||
|
|
||||||
log.Info().Str("subnet", subnet).Msg("Starting manual discovery scan")
|
log.Info().Str("subnet", subnet).Msg("Starting manual discovery scan")
|
||||||
|
|
||||||
scanner := discovery.NewScanner()
|
scanner, buildErr := discoveryinternal.BuildScanner(h.config.Discovery)
|
||||||
|
if buildErr != nil {
|
||||||
|
log.Warn().Err(buildErr).Msg("Falling back to default scanner for manual discovery")
|
||||||
|
scanner = pkgdiscovery.NewScanner()
|
||||||
|
}
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
|
@ -3594,6 +3600,40 @@ SSH_SENSORS_PUBLIC_KEY="%s"
|
||||||
SSH_PROXY_KEY_ENTRY="restrict,permitopen=\"*:22\" $SSH_PROXY_PUBLIC_KEY # pulse-proxyjump"
|
SSH_PROXY_KEY_ENTRY="restrict,permitopen=\"*:22\" $SSH_PROXY_PUBLIC_KEY # pulse-proxyjump"
|
||||||
SSH_SENSORS_KEY_ENTRY="command=\"sensors -j\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty $SSH_SENSORS_PUBLIC_KEY # pulse-sensors"
|
SSH_SENSORS_KEY_ENTRY="command=\"sensors -j\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty $SSH_SENSORS_PUBLIC_KEY # pulse-sensors"
|
||||||
TEMPERATURE_ENABLED=false
|
TEMPERATURE_ENABLED=false
|
||||||
|
TEMP_MONITORING_AVAILABLE=true
|
||||||
|
MIN_PROXY_VERSION="%s"
|
||||||
|
PULSE_VERSION_ENDPOINT="%s/api/version"
|
||||||
|
|
||||||
|
version_ge() {
|
||||||
|
if command -v dpkg >/dev/null 2>&1; then
|
||||||
|
dpkg --compare-versions "$1" ge "$2"
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
if command -v sort >/dev/null 2>&1; then
|
||||||
|
[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n1)" = "$1" ]
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
[ "$1" = "$2" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
PULSE_VERSION=$(curl -s -f "$PULSE_VERSION_ENDPOINT" 2>/dev/null | awk -F'"' '/"version":/{print $4}' | head -n1)
|
||||||
|
if [ -z "$PULSE_VERSION" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ Could not determine Pulse version from $PULSE_VERSION_ENDPOINT"
|
||||||
|
echo " Temperature proxy requires Pulse $MIN_PROXY_VERSION or later."
|
||||||
|
TEMP_MONITORING_AVAILABLE=false
|
||||||
|
elif ! version_ge "$PULSE_VERSION" "$MIN_PROXY_VERSION"; then
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "⚠️ Pulse upgrade required for temperature proxy"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Detected Pulse version: $PULSE_VERSION"
|
||||||
|
echo "Minimum required version: $MIN_PROXY_VERSION"
|
||||||
|
echo ""
|
||||||
|
echo "Please upgrade the Pulse container before rerunning this setup script."
|
||||||
|
echo ""
|
||||||
|
TEMP_MONITORING_AVAILABLE=false
|
||||||
|
fi
|
||||||
|
|
||||||
# Check if temperature proxy is available and override SSH key if it is
|
# Check if temperature proxy is available and override SSH key if it is
|
||||||
PROXY_KEY_URL="%s/api/system/proxy-public-key"
|
PROXY_KEY_URL="%s/api/system/proxy-public-key"
|
||||||
|
|
@ -3640,11 +3680,10 @@ if command -v pct >/dev/null 2>&1; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Track whether temperature monitoring can work
|
# Track whether temperature monitoring can work (may be disabled by checks above)
|
||||||
TEMP_MONITORING_AVAILABLE=true
|
|
||||||
|
|
||||||
# If Pulse is containerized, try to install proxy automatically
|
# If Pulse is containerized, try to install proxy automatically
|
||||||
if [ "$PULSE_IS_CONTAINERIZED" = true ] && [ -n "$PULSE_CTID" ]; then
|
if [ "$TEMP_MONITORING_AVAILABLE" = true ] && [ "$PULSE_IS_CONTAINERIZED" = true ] && [ -n "$PULSE_CTID" ]; then
|
||||||
# Try automatic installation - proxy keeps SSH credentials on the host for security
|
# Try automatic installation - proxy keeps SSH credentials on the host for security
|
||||||
if true; then
|
if true; then
|
||||||
# Download installer script from Pulse server
|
# Download installer script from Pulse server
|
||||||
|
|
@ -3677,42 +3716,91 @@ if [ "$PULSE_IS_CONTAINERIZED" = true ] && [ -n "$PULSE_CTID" ]; then
|
||||||
echo "✓ Secure proxy architecture enabled"
|
echo "✓ Secure proxy architecture enabled"
|
||||||
echo " SSH keys are managed on the host for enhanced security"
|
echo " SSH keys are managed on the host for enhanced security"
|
||||||
echo ""
|
echo ""
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ pulse-sensor-proxy service is not active. Check logs with:"
|
||||||
|
echo " journalctl -u pulse-sensor-proxy -n 40"
|
||||||
|
TEMP_MONITORING_AVAILABLE=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Configure socket bind mount and restart container automatically
|
if [ "$TEMP_MONITORING_AVAILABLE" = true ] && [ ! -S /run/pulse-sensor-proxy/pulse-sensor-proxy.sock ]; then
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo " ✗ Proxy socket not found at /run/pulse-sensor-proxy/pulse-sensor-proxy.sock"
|
||||||
echo "Finalizing Setup"
|
echo " Check logs with: journalctl -u pulse-sensor-proxy -n 40"
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
TEMP_MONITORING_AVAILABLE=false
|
||||||
echo ""
|
fi
|
||||||
echo "Configuring socket bind mount for container $PULSE_CTID..."
|
|
||||||
|
|
||||||
# Configure bind mount for proxy socket
|
if [ "$TEMP_MONITORING_AVAILABLE" = true ]; then
|
||||||
pct set "$PULSE_CTID" -mp0 /run/pulse-sensor-proxy,mp=/mnt/pulse-proxy
|
# Configure socket bind mount and restart container automatically
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
# Check if container is currently running
|
echo "Finalizing Setup"
|
||||||
if pct status "$PULSE_CTID" 2>/dev/null | grep -q "running"; then
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
echo ""
|
echo ""
|
||||||
echo "⚠️ Container $PULSE_CTID is currently running."
|
echo "Configuring socket bind mount for container $PULSE_CTID..."
|
||||||
echo " The proxy socket will be available after a restart."
|
|
||||||
echo ""
|
|
||||||
echo " Restart manually when ready:"
|
|
||||||
echo " pct stop $PULSE_CTID && sleep 2 && pct start $PULSE_CTID"
|
|
||||||
echo ""
|
|
||||||
echo " Or the socket may be hot-plugged automatically (LXC 4.0+)"
|
|
||||||
echo ""
|
|
||||||
else
|
|
||||||
echo "Container is stopped, starting it now..."
|
|
||||||
|
|
||||||
# Set up trap to restart container even if script is interrupted
|
# Configure bind mount for proxy socket
|
||||||
trap "echo 'Starting container before exit...'; pct start $PULSE_CTID 2>/dev/null || true" EXIT INT TERM
|
pct set "$PULSE_CTID" -mp0 /run/pulse-sensor-proxy,mp=/mnt/pulse-proxy
|
||||||
|
|
||||||
pct start "$PULSE_CTID"
|
CONTAINER_RESTARTED=false
|
||||||
|
# Check if container is currently running
|
||||||
|
if pct status "$PULSE_CTID" 2>/dev/null | grep -q "running"; then
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ Container $PULSE_CTID must be restarted to expose the proxy socket."
|
||||||
|
echo "Restart now? [Y/n]"
|
||||||
|
printf "> "
|
||||||
|
if [ -t 0 ]; then
|
||||||
|
read -n 1 -r RESTART_REPLY
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
if read -n 1 -r RESTART_REPLY </dev/tty 2>/dev/null; then
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
RESTART_REPLY="n"
|
||||||
|
echo "(No terminal available - skipping automatic restart)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
RESTART_REPLY=${RESTART_REPLY:-Y}
|
||||||
|
if [[ "$RESTART_REPLY" =~ ^[Yy]$ ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Restarting container $PULSE_CTID..."
|
||||||
|
if pct stop "$PULSE_CTID" && sleep 2 && pct start "$PULSE_CTID"; then
|
||||||
|
echo " ✓ Container restarted"
|
||||||
|
CONTAINER_RESTARTED=true
|
||||||
|
else
|
||||||
|
echo " ✗ Failed to restart container automatically."
|
||||||
|
echo " Please run: pct stop $PULSE_CTID && sleep 2 && pct start $PULSE_CTID"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "Please restart manually when ready:"
|
||||||
|
echo " pct stop $PULSE_CTID && sleep 2 && pct start $PULSE_CTID"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Container is stopped, starting it now..."
|
||||||
|
|
||||||
# Clear the trap after successful start
|
# Set up trap to restart container even if script is interrupted
|
||||||
trap - EXIT INT TERM
|
trap "echo 'Starting container before exit...'; pct start $PULSE_CTID 2>/dev/null || true" EXIT INT TERM
|
||||||
|
|
||||||
echo " ✓ Container started successfully"
|
if pct start "$PULSE_CTID"; then
|
||||||
echo ""
|
CONTAINER_RESTARTED=true
|
||||||
|
echo " ✓ Container started successfully"
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo " ✗ Failed to start container $PULSE_CTID automatically."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clear the trap after successful start
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$CONTAINER_RESTARTED" = true ]; then
|
||||||
|
if ! pct exec "$PULSE_CTID" -- test -S /mnt/pulse-proxy/pulse-sensor-proxy.sock 2>/dev/null; then
|
||||||
|
echo " ✗ Proxy socket not visible inside the container (/mnt/pulse-proxy/pulse-sensor-proxy.sock)"
|
||||||
|
echo " Verify the bind mount and container restart completed successfully."
|
||||||
|
TEMP_MONITORING_AVAILABLE=false
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
TEMP_MONITORING_AVAILABLE=false
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo ""
|
echo ""
|
||||||
|
|
@ -4326,7 +4414,7 @@ if [ "$AUTO_REG_SUCCESS" != true ]; then
|
||||||
fi
|
fi
|
||||||
`, serverName, time.Now().Format("2006-01-02 15:04:05"), pulseIP,
|
`, serverName, time.Now().Format("2006-01-02 15:04:05"), pulseIP,
|
||||||
tokenName, tokenName, tokenName, tokenName, tokenName, tokenName,
|
tokenName, tokenName, tokenName, tokenName, tokenName, tokenName,
|
||||||
authToken, pulseURL, serverHost, tokenName, tokenName, storagePerms, sshKeys.ProxyPublicKey, sshKeys.SensorsPublicKey, pulseURL, pulseURL, pulseURL, pulseURL, pulseURL, pulseURL, authToken, pulseURL, authToken, pulseURL, tokenName)
|
authToken, pulseURL, serverHost, tokenName, tokenName, storagePerms, sshKeys.ProxyPublicKey, sshKeys.SensorsPublicKey, minProxyReadyVersion, pulseURL, pulseURL, pulseURL, pulseURL, pulseURL, pulseURL, authToken, pulseURL, authToken, pulseURL, tokenName)
|
||||||
|
|
||||||
} else { // PBS
|
} else { // PBS
|
||||||
script = fmt.Sprintf(`#!/bin/bash
|
script = fmt.Sprintf(`#!/bin/bash
|
||||||
|
|
|
||||||
|
|
@ -156,15 +156,14 @@ func handleQuickSecuritySetupFixed(r *Router) http.HandlerFunc {
|
||||||
}
|
}
|
||||||
log.Info().Msg("Runtime config updated with new security settings - active immediately")
|
log.Info().Msg("Runtime config updated with new security settings - active immediately")
|
||||||
|
|
||||||
// Save system settings to system.json
|
// Save system settings to system.json
|
||||||
systemSettings := config.SystemSettings{
|
systemSettings := config.DefaultSystemSettings()
|
||||||
ConnectionTimeout: 10, // Default
|
systemSettings.ConnectionTimeout = 10 // Default seconds
|
||||||
AutoUpdateEnabled: false, // Default
|
systemSettings.AutoUpdateEnabled = false // Default disabled
|
||||||
}
|
if err := r.persistence.SaveSystemSettings(*systemSettings); err != nil {
|
||||||
if err := r.persistence.SaveSystemSettings(systemSettings); err != nil {
|
log.Error().Err(err).Msg("Failed to save system settings")
|
||||||
log.Error().Err(err).Msg("Failed to save system settings")
|
// Continue anyway - not critical for auth setup
|
||||||
// Continue anyway - not critical for auth setup
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Detect environment
|
// Detect environment
|
||||||
isSystemd := os.Getenv("INVOCATION_ID") != ""
|
isSystemd := os.Getenv("INVOCATION_ID") != ""
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -152,6 +153,107 @@ func validateSystemSettings(settings *config.SystemSettings, rawRequest map[stri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if val, ok := rawRequest["discoveryConfig"]; ok {
|
||||||
|
cfgMap, ok := val.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("discoveryConfig must be an object")
|
||||||
|
}
|
||||||
|
|
||||||
|
if envVal, exists := cfgMap["environmentOverride"]; exists {
|
||||||
|
envStr, ok := envVal.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("discoveryConfig.environmentOverride must be a string")
|
||||||
|
}
|
||||||
|
if !config.IsValidDiscoveryEnvironment(envStr) {
|
||||||
|
return fmt.Errorf("invalid discovery environment override: %s", envStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allowVal, exists := cfgMap["subnetAllowlist"]; exists {
|
||||||
|
items, ok := allowVal.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("discoveryConfig.subnetAllowlist must be an array of CIDR strings")
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
cidr, ok := item.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("discoveryConfig.subnetAllowlist entries must be strings")
|
||||||
|
}
|
||||||
|
if _, _, err := net.ParseCIDR(cidr); err != nil {
|
||||||
|
return fmt.Errorf("invalid CIDR in discoveryConfig.subnetAllowlist: %s", cidr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if blockVal, exists := cfgMap["subnetBlocklist"]; exists {
|
||||||
|
items, ok := blockVal.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("discoveryConfig.subnetBlocklist must be an array of CIDR strings")
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
cidr, ok := item.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("discoveryConfig.subnetBlocklist entries must be strings")
|
||||||
|
}
|
||||||
|
if _, _, err := net.ParseCIDR(cidr); err != nil {
|
||||||
|
return fmt.Errorf("invalid CIDR in discoveryConfig.subnetBlocklist: %s", cidr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hostsVal, exists := cfgMap["maxHostsPerScan"]; exists {
|
||||||
|
value, ok := hostsVal.(float64)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("discoveryConfig.maxHostsPerScan must be a number")
|
||||||
|
}
|
||||||
|
if value <= 0 {
|
||||||
|
return fmt.Errorf("discoveryConfig.maxHostsPerScan must be greater than zero")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if concurrentVal, exists := cfgMap["maxConcurrent"]; exists {
|
||||||
|
value, ok := concurrentVal.(float64)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("discoveryConfig.maxConcurrent must be a number")
|
||||||
|
}
|
||||||
|
if value <= 0 || value > 1000 {
|
||||||
|
return fmt.Errorf("discoveryConfig.maxConcurrent must be between 1 and 1000")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, exists := cfgMap["enableReverseDns"]; exists {
|
||||||
|
if _, ok := val.(bool); !ok {
|
||||||
|
return fmt.Errorf("discoveryConfig.enableReverseDns must be a boolean")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, exists := cfgMap["scanGateways"]; exists {
|
||||||
|
if _, ok := val.(bool); !ok {
|
||||||
|
return fmt.Errorf("discoveryConfig.scanGateways must be a boolean")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, exists := cfgMap["dialTimeoutMs"]; exists {
|
||||||
|
timeout, ok := val.(float64)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("discoveryConfig.dialTimeoutMs must be a number")
|
||||||
|
}
|
||||||
|
if timeout <= 0 {
|
||||||
|
return fmt.Errorf("discoveryConfig.dialTimeoutMs must be greater than zero")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, exists := cfgMap["httpTimeoutMs"]; exists {
|
||||||
|
timeout, ok := val.(float64)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("discoveryConfig.httpTimeoutMs must be a number")
|
||||||
|
}
|
||||||
|
if timeout <= 0 {
|
||||||
|
return fmt.Errorf("discoveryConfig.httpTimeoutMs must be greater than zero")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate connection timeout (min 1 second, max 5 minutes)
|
// Validate connection timeout (min 1 second, max 5 minutes)
|
||||||
if val, ok := rawRequest["connectionTimeout"]; ok {
|
if val, ok := rawRequest["connectionTimeout"]; ok {
|
||||||
if timeout, ok := val.(float64); ok {
|
if timeout, ok := val.(float64); ok {
|
||||||
|
|
@ -204,7 +306,10 @@ func (h *SystemSettingsHandler) HandleGetSystemSettings(w http.ResponseWriter, r
|
||||||
settings, err := h.persistence.LoadSystemSettings()
|
settings, err := h.persistence.LoadSystemSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to load system settings")
|
log.Error().Err(err).Msg("Failed to load system settings")
|
||||||
settings = &config.SystemSettings{}
|
settings = config.DefaultSystemSettings()
|
||||||
|
}
|
||||||
|
if settings == nil {
|
||||||
|
settings = config.DefaultSystemSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log loaded settings for debugging
|
// Log loaded settings for debugging
|
||||||
|
|
@ -217,6 +322,7 @@ func (h *SystemSettingsHandler) HandleGetSystemSettings(w http.ResponseWriter, r
|
||||||
settings.BackupPollingInterval = int(h.config.BackupPollingInterval.Seconds())
|
settings.BackupPollingInterval = int(h.config.BackupPollingInterval.Seconds())
|
||||||
enabled := h.config.EnableBackupPolling
|
enabled := h.config.EnableBackupPolling
|
||||||
settings.BackupPollingEnabled = &enabled
|
settings.BackupPollingEnabled = &enabled
|
||||||
|
settings.DiscoveryConfig = config.CloneDiscoveryConfig(h.config.Discovery)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include env override information
|
// Include env override information
|
||||||
|
|
@ -268,10 +374,10 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
||||||
existingSettings, err := h.persistence.LoadSystemSettings()
|
existingSettings, err := h.persistence.LoadSystemSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to load existing settings")
|
log.Error().Err(err).Msg("Failed to load existing settings")
|
||||||
existingSettings = &config.SystemSettings{}
|
existingSettings = config.DefaultSystemSettings()
|
||||||
}
|
}
|
||||||
if existingSettings == nil {
|
if existingSettings == nil {
|
||||||
existingSettings = &config.SystemSettings{}
|
existingSettings = config.DefaultSystemSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the request body into a map to check which fields were provided
|
// Read the request body into a map to check which fields were provided
|
||||||
|
|
@ -303,6 +409,7 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
||||||
|
|
||||||
// Start with existing settings
|
// Start with existing settings
|
||||||
settings := *existingSettings
|
settings := *existingSettings
|
||||||
|
discoveryConfigUpdated := false
|
||||||
|
|
||||||
// Only update fields that were provided in the request
|
// Only update fields that were provided in the request
|
||||||
// Note: PVE polling is hardcoded to 10s, legacy polling fields are ignored
|
// Note: PVE polling is hardcoded to 10s, legacy polling fields are ignored
|
||||||
|
|
@ -336,6 +443,10 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
||||||
if updates.DiscoverySubnet != "" {
|
if updates.DiscoverySubnet != "" {
|
||||||
settings.DiscoverySubnet = updates.DiscoverySubnet
|
settings.DiscoverySubnet = updates.DiscoverySubnet
|
||||||
}
|
}
|
||||||
|
if _, ok := rawRequest["discoveryConfig"]; ok {
|
||||||
|
settings.DiscoveryConfig = config.CloneDiscoveryConfig(updates.DiscoveryConfig)
|
||||||
|
discoveryConfigUpdated = true
|
||||||
|
}
|
||||||
// Allow clearing of AllowedEmbedOrigins by setting to empty string
|
// Allow clearing of AllowedEmbedOrigins by setting to empty string
|
||||||
if _, ok := rawRequest["allowedEmbedOrigins"]; ok {
|
if _, ok := rawRequest["allowedEmbedOrigins"]; ok {
|
||||||
settings.AllowedEmbedOrigins = updates.AllowedEmbedOrigins
|
settings.AllowedEmbedOrigins = updates.AllowedEmbedOrigins
|
||||||
|
|
@ -401,6 +512,7 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
||||||
if settings.DiscoverySubnet != "" {
|
if settings.DiscoverySubnet != "" {
|
||||||
h.config.DiscoverySubnet = settings.DiscoverySubnet
|
h.config.DiscoverySubnet = settings.DiscoverySubnet
|
||||||
}
|
}
|
||||||
|
h.config.Discovery = config.CloneDiscoveryConfig(settings.DiscoveryConfig)
|
||||||
|
|
||||||
// Start or stop discovery service based on setting change
|
// Start or stop discovery service based on setting change
|
||||||
if h.monitor != nil {
|
if h.monitor != nil {
|
||||||
|
|
@ -422,6 +534,12 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
||||||
svc.SetSubnet(settings.DiscoverySubnet)
|
svc.SetSubnet(settings.DiscoverySubnet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if discoveryConfigUpdated && settings.DiscoveryEnabled {
|
||||||
|
if svc := h.monitor.GetDiscoveryService(); svc != nil {
|
||||||
|
log.Info().Msg("Discovery configuration changed; triggering refresh")
|
||||||
|
svc.ForceRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to persistence
|
// Save to persistence
|
||||||
|
|
|
||||||
|
|
@ -130,8 +130,9 @@ type Config struct {
|
||||||
AutoUpdateTime string `envconfig:"AUTO_UPDATE_TIME" default:"03:00"`
|
AutoUpdateTime string `envconfig:"AUTO_UPDATE_TIME" default:"03:00"`
|
||||||
|
|
||||||
// Discovery settings
|
// Discovery settings
|
||||||
DiscoveryEnabled bool `envconfig:"DISCOVERY_ENABLED" default:"true"`
|
DiscoveryEnabled bool `envconfig:"DISCOVERY_ENABLED" default:"true"`
|
||||||
DiscoverySubnet string `envconfig:"DISCOVERY_SUBNET" default:"auto"`
|
DiscoverySubnet string `envconfig:"DISCOVERY_SUBNET" default:"auto"`
|
||||||
|
Discovery DiscoveryConfig `json:"discoveryConfig"`
|
||||||
|
|
||||||
// Deprecated - for backward compatibility
|
// Deprecated - for backward compatibility
|
||||||
Port int `envconfig:"PORT"` // Maps to BackendPort
|
Port int `envconfig:"PORT"` // Maps to BackendPort
|
||||||
|
|
@ -141,6 +142,71 @@ type Config struct {
|
||||||
EnvOverrides map[string]bool `json:"-"`
|
EnvOverrides map[string]bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DiscoveryConfig captures overrides for network discovery behaviour.
|
||||||
|
type DiscoveryConfig struct {
|
||||||
|
EnvironmentOverride string `json:"environmentOverride,omitempty"`
|
||||||
|
SubnetAllowlist []string `json:"subnetAllowlist,omitempty"`
|
||||||
|
SubnetBlocklist []string `json:"subnetBlocklist,omitempty"`
|
||||||
|
MaxHostsPerScan int `json:"maxHostsPerScan,omitempty"`
|
||||||
|
MaxConcurrent int `json:"maxConcurrent,omitempty"`
|
||||||
|
EnableReverseDNS bool `json:"enableReverseDns"`
|
||||||
|
ScanGateways bool `json:"scanGateways"`
|
||||||
|
DialTimeout int `json:"dialTimeoutMs,omitempty"`
|
||||||
|
HTTPTimeout int `json:"httpTimeoutMs,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultDiscoveryConfig returns opinionated defaults for discovery behaviour.
|
||||||
|
func DefaultDiscoveryConfig() DiscoveryConfig {
|
||||||
|
return DiscoveryConfig{
|
||||||
|
EnvironmentOverride: "auto",
|
||||||
|
SubnetAllowlist: []string{},
|
||||||
|
SubnetBlocklist: []string{"169.254.0.0/16"},
|
||||||
|
MaxHostsPerScan: 1024,
|
||||||
|
MaxConcurrent: 50,
|
||||||
|
EnableReverseDNS: true,
|
||||||
|
ScanGateways: true,
|
||||||
|
DialTimeout: 1000,
|
||||||
|
HTTPTimeout: 2000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloneDiscoveryConfig returns a deep copy of the provided discovery config.
|
||||||
|
func CloneDiscoveryConfig(cfg DiscoveryConfig) DiscoveryConfig {
|
||||||
|
clone := cfg
|
||||||
|
if cfg.SubnetAllowlist != nil {
|
||||||
|
clone.SubnetAllowlist = append([]string(nil), cfg.SubnetAllowlist...)
|
||||||
|
}
|
||||||
|
if cfg.SubnetBlocklist != nil {
|
||||||
|
clone.SubnetBlocklist = append([]string(nil), cfg.SubnetBlocklist...)
|
||||||
|
}
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidDiscoveryEnvironment reports whether the supplied override is recognised.
|
||||||
|
func IsValidDiscoveryEnvironment(value string) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||||
|
case "", "auto", "native", "docker_host", "docker_bridge", "lxc_privileged", "lxc_unprivileged":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitAndTrim(value string) []string {
|
||||||
|
if value == "" {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
parts := strings.Split(value, ",")
|
||||||
|
result := make([]string, 0, len(parts))
|
||||||
|
for _, part := range parts {
|
||||||
|
trimmed := strings.TrimSpace(part)
|
||||||
|
if trimmed != "" {
|
||||||
|
result = append(result, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// PVEInstance represents a Proxmox VE connection
|
// PVEInstance represents a Proxmox VE connection
|
||||||
type PVEInstance struct {
|
type PVEInstance struct {
|
||||||
Name string
|
Name string
|
||||||
|
|
@ -284,9 +350,11 @@ func Load() (*Config, error) {
|
||||||
DiscoveryEnabled: true,
|
DiscoveryEnabled: true,
|
||||||
DiscoverySubnet: "auto",
|
DiscoverySubnet: "auto",
|
||||||
EnvOverrides: make(map[string]bool),
|
EnvOverrides: make(map[string]bool),
|
||||||
OIDC: NewOIDCConfig(),
|
OIDC: NewOIDCConfig(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg.Discovery = DefaultDiscoveryConfig()
|
||||||
|
|
||||||
// Initialize persistence
|
// Initialize persistence
|
||||||
persistence := NewConfigPersistence(dataDir)
|
persistence := NewConfigPersistence(dataDir)
|
||||||
if persistence != nil {
|
if persistence != nil {
|
||||||
|
|
@ -361,6 +429,7 @@ func Load() (*Config, error) {
|
||||||
if systemSettings.DiscoverySubnet != "" {
|
if systemSettings.DiscoverySubnet != "" {
|
||||||
cfg.DiscoverySubnet = systemSettings.DiscoverySubnet
|
cfg.DiscoverySubnet = systemSettings.DiscoverySubnet
|
||||||
}
|
}
|
||||||
|
cfg.Discovery = CloneDiscoveryConfig(systemSettings.DiscoveryConfig)
|
||||||
// APIToken no longer loaded from system.json - only from .env
|
// APIToken no longer loaded from system.json - only from .env
|
||||||
log.Info().
|
log.Info().
|
||||||
Str("updateChannel", cfg.UpdateChannel).
|
Str("updateChannel", cfg.UpdateChannel).
|
||||||
|
|
@ -369,13 +438,11 @@ func Load() (*Config, error) {
|
||||||
} else {
|
} else {
|
||||||
// No system.json exists - create default one
|
// No system.json exists - create default one
|
||||||
log.Info().Msg("No system.json found, creating default")
|
log.Info().Msg("No system.json found, creating default")
|
||||||
defaultSettings := SystemSettings{
|
defaultSettings := DefaultSystemSettings()
|
||||||
ConnectionTimeout: int(cfg.ConnectionTimeout.Seconds()),
|
defaultSettings.ConnectionTimeout = int(cfg.ConnectionTimeout.Seconds())
|
||||||
AutoUpdateEnabled: false,
|
if err := persistence.SaveSystemSettings(*defaultSettings); err != nil {
|
||||||
}
|
log.Warn().Err(err).Msg("Failed to create default system.json")
|
||||||
if err := persistence.SaveSystemSettings(defaultSettings); err != nil {
|
}
|
||||||
log.Warn().Err(err).Msg("Failed to create default system.json")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if oidcSettings, err := persistence.LoadOIDCConfig(); err == nil && oidcSettings != nil {
|
if oidcSettings, err := persistence.LoadOIDCConfig(); err == nil && oidcSettings != nil {
|
||||||
|
|
@ -741,6 +808,83 @@ func Load() (*Config, error) {
|
||||||
cfg.EnvOverrides["discoverySubnet"] = true
|
cfg.EnvOverrides["discoverySubnet"] = true
|
||||||
log.Info().Str("subnet", discoverySubnet).Msg("Discovery subnet overridden by DISCOVERY_SUBNET env var")
|
log.Info().Str("subnet", discoverySubnet).Msg("Discovery subnet overridden by DISCOVERY_SUBNET env var")
|
||||||
}
|
}
|
||||||
|
if envOverride := strings.TrimSpace(os.Getenv("DISCOVERY_ENVIRONMENT_OVERRIDE")); envOverride != "" {
|
||||||
|
if IsValidDiscoveryEnvironment(envOverride) {
|
||||||
|
cfg.Discovery.EnvironmentOverride = strings.ToLower(envOverride)
|
||||||
|
cfg.EnvOverrides["discoveryEnvironmentOverride"] = true
|
||||||
|
log.Info().Str("environment", cfg.Discovery.EnvironmentOverride).Msg("Discovery environment override set by DISCOVERY_ENVIRONMENT_OVERRIDE")
|
||||||
|
} else {
|
||||||
|
log.Warn().Str("value", envOverride).Msg("Ignoring invalid DISCOVERY_ENVIRONMENT_OVERRIDE value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allowlistEnv := strings.TrimSpace(os.Getenv("DISCOVERY_SUBNET_ALLOWLIST")); allowlistEnv != "" {
|
||||||
|
parts := splitAndTrim(allowlistEnv)
|
||||||
|
cfg.Discovery.SubnetAllowlist = parts
|
||||||
|
cfg.EnvOverrides["discoverySubnetAllowlist"] = true
|
||||||
|
log.Info().Int("allowlistCount", len(parts)).Msg("Discovery subnet allowlist overridden by DISCOVERY_SUBNET_ALLOWLIST")
|
||||||
|
}
|
||||||
|
if blocklistEnv := strings.TrimSpace(os.Getenv("DISCOVERY_SUBNET_BLOCKLIST")); blocklistEnv != "" {
|
||||||
|
parts := splitAndTrim(blocklistEnv)
|
||||||
|
cfg.Discovery.SubnetBlocklist = parts
|
||||||
|
cfg.EnvOverrides["discoverySubnetBlocklist"] = true
|
||||||
|
log.Info().Int("blocklistCount", len(parts)).Msg("Discovery subnet blocklist overridden by DISCOVERY_SUBNET_BLOCKLIST")
|
||||||
|
}
|
||||||
|
if maxHostsEnv := strings.TrimSpace(os.Getenv("DISCOVERY_MAX_HOSTS_PER_SCAN")); maxHostsEnv != "" {
|
||||||
|
if v, err := strconv.Atoi(maxHostsEnv); err == nil && v > 0 {
|
||||||
|
cfg.Discovery.MaxHostsPerScan = v
|
||||||
|
cfg.EnvOverrides["discoveryMaxHostsPerScan"] = true
|
||||||
|
log.Info().Int("maxHostsPerScan", v).Msg("Discovery max hosts per scan overridden by DISCOVERY_MAX_HOSTS_PER_SCAN")
|
||||||
|
} else {
|
||||||
|
log.Warn().Str("value", maxHostsEnv).Msg("Ignoring invalid DISCOVERY_MAX_HOSTS_PER_SCAN value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if maxConcurrentEnv := strings.TrimSpace(os.Getenv("DISCOVERY_MAX_CONCURRENT")); maxConcurrentEnv != "" {
|
||||||
|
if v, err := strconv.Atoi(maxConcurrentEnv); err == nil && v > 0 {
|
||||||
|
cfg.Discovery.MaxConcurrent = v
|
||||||
|
cfg.EnvOverrides["discoveryMaxConcurrent"] = true
|
||||||
|
log.Info().Int("maxConcurrent", v).Msg("Discovery concurrency overridden by DISCOVERY_MAX_CONCURRENT")
|
||||||
|
} else {
|
||||||
|
log.Warn().Str("value", maxConcurrentEnv).Msg("Ignoring invalid DISCOVERY_MAX_CONCURRENT value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if reverseDNSEnv := strings.TrimSpace(os.Getenv("DISCOVERY_ENABLE_REVERSE_DNS")); reverseDNSEnv != "" {
|
||||||
|
switch strings.ToLower(reverseDNSEnv) {
|
||||||
|
case "0", "false", "no", "off":
|
||||||
|
cfg.Discovery.EnableReverseDNS = false
|
||||||
|
default:
|
||||||
|
cfg.Discovery.EnableReverseDNS = true
|
||||||
|
}
|
||||||
|
cfg.EnvOverrides["discoveryEnableReverseDns"] = true
|
||||||
|
log.Info().Bool("enableReverseDNS", cfg.Discovery.EnableReverseDNS).Msg("Discovery reverse DNS setting overridden by DISCOVERY_ENABLE_REVERSE_DNS")
|
||||||
|
}
|
||||||
|
if scanGatewaysEnv := strings.TrimSpace(os.Getenv("DISCOVERY_SCAN_GATEWAYS")); scanGatewaysEnv != "" {
|
||||||
|
switch strings.ToLower(scanGatewaysEnv) {
|
||||||
|
case "0", "false", "no", "off":
|
||||||
|
cfg.Discovery.ScanGateways = false
|
||||||
|
default:
|
||||||
|
cfg.Discovery.ScanGateways = true
|
||||||
|
}
|
||||||
|
cfg.EnvOverrides["discoveryScanGateways"] = true
|
||||||
|
log.Info().Bool("scanGateways", cfg.Discovery.ScanGateways).Msg("Discovery gateway scanning overridden by DISCOVERY_SCAN_GATEWAYS")
|
||||||
|
}
|
||||||
|
if dialTimeoutEnv := strings.TrimSpace(os.Getenv("DISCOVERY_DIAL_TIMEOUT_MS")); dialTimeoutEnv != "" {
|
||||||
|
if v, err := strconv.Atoi(dialTimeoutEnv); err == nil && v > 0 {
|
||||||
|
cfg.Discovery.DialTimeout = v
|
||||||
|
cfg.EnvOverrides["discoveryDialTimeoutMs"] = true
|
||||||
|
log.Info().Int("dialTimeoutMs", v).Msg("Discovery dial timeout overridden by DISCOVERY_DIAL_TIMEOUT_MS")
|
||||||
|
} else {
|
||||||
|
log.Warn().Str("value", dialTimeoutEnv).Msg("Ignoring invalid DISCOVERY_DIAL_TIMEOUT_MS value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if httpTimeoutEnv := strings.TrimSpace(os.Getenv("DISCOVERY_HTTP_TIMEOUT_MS")); httpTimeoutEnv != "" {
|
||||||
|
if v, err := strconv.Atoi(httpTimeoutEnv); err == nil && v > 0 {
|
||||||
|
cfg.Discovery.HTTPTimeout = v
|
||||||
|
cfg.EnvOverrides["discoveryHttpTimeoutMs"] = true
|
||||||
|
log.Info().Int("httpTimeoutMs", v).Msg("Discovery HTTP timeout overridden by DISCOVERY_HTTP_TIMEOUT_MS")
|
||||||
|
} else {
|
||||||
|
log.Warn().Str("value", httpTimeoutEnv).Msg("Ignoring invalid DISCOVERY_HTTP_TIMEOUT_MS value")
|
||||||
|
}
|
||||||
|
}
|
||||||
if logLevel := os.Getenv("LOG_LEVEL"); logLevel != "" {
|
if logLevel := os.Getenv("LOG_LEVEL"); logLevel != "" {
|
||||||
cfg.LogLevel = logLevel
|
cfg.LogLevel = logLevel
|
||||||
cfg.EnvOverrides["logLevel"] = true
|
cfg.EnvOverrides["logLevel"] = true
|
||||||
|
|
@ -820,6 +964,7 @@ func SaveConfig(cfg *Config) error {
|
||||||
LogLevel: cfg.LogLevel,
|
LogLevel: cfg.LogLevel,
|
||||||
DiscoveryEnabled: cfg.DiscoveryEnabled,
|
DiscoveryEnabled: cfg.DiscoveryEnabled,
|
||||||
DiscoverySubnet: cfg.DiscoverySubnet,
|
DiscoverySubnet: cfg.DiscoverySubnet,
|
||||||
|
DiscoveryConfig: CloneDiscoveryConfig(cfg.Discovery),
|
||||||
AdaptivePollingEnabled: &adaptiveEnabled,
|
AdaptivePollingEnabled: &adaptiveEnabled,
|
||||||
AdaptivePollingBaseInterval: int(cfg.AdaptivePollingBaseInterval / time.Second),
|
AdaptivePollingBaseInterval: int(cfg.AdaptivePollingBaseInterval / time.Second),
|
||||||
AdaptivePollingMinInterval: int(cfg.AdaptivePollingMinInterval / time.Second),
|
AdaptivePollingMinInterval: int(cfg.AdaptivePollingMinInterval / time.Second),
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ func (c *ConfigPersistence) ExportConfig(passphrase string) (string, error) {
|
||||||
return "", fmt.Errorf("failed to load system settings: %w", err)
|
return "", fmt.Errorf("failed to load system settings: %w", err)
|
||||||
}
|
}
|
||||||
if systemSettings == nil {
|
if systemSettings == nil {
|
||||||
systemSettings = &SystemSettings{}
|
systemSettings = DefaultSystemSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
oidcConfig, err := c.LoadOIDCConfig()
|
oidcConfig, err := c.LoadOIDCConfig()
|
||||||
|
|
|
||||||
|
|
@ -682,12 +682,27 @@ type SystemSettings struct {
|
||||||
LogLevel string `json:"logLevel,omitempty"`
|
LogLevel string `json:"logLevel,omitempty"`
|
||||||
DiscoveryEnabled bool `json:"discoveryEnabled"`
|
DiscoveryEnabled bool `json:"discoveryEnabled"`
|
||||||
DiscoverySubnet string `json:"discoverySubnet,omitempty"`
|
DiscoverySubnet string `json:"discoverySubnet,omitempty"`
|
||||||
|
DiscoveryConfig DiscoveryConfig `json:"discoveryConfig"`
|
||||||
Theme string `json:"theme,omitempty"` // User theme preference: "light", "dark", or empty for system default
|
Theme string `json:"theme,omitempty"` // User theme preference: "light", "dark", or empty for system default
|
||||||
AllowEmbedding bool `json:"allowEmbedding"` // Allow iframe embedding
|
AllowEmbedding bool `json:"allowEmbedding"` // Allow iframe embedding
|
||||||
AllowedEmbedOrigins string `json:"allowedEmbedOrigins,omitempty"` // Comma-separated list of allowed origins for embedding
|
AllowedEmbedOrigins string `json:"allowedEmbedOrigins,omitempty"` // Comma-separated list of allowed origins for embedding
|
||||||
// APIToken removed - now handled via .env file only
|
// APIToken removed - now handled via .env file only
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultSystemSettings returns a SystemSettings struct populated with sane defaults.
|
||||||
|
func DefaultSystemSettings() *SystemSettings {
|
||||||
|
defaultDiscovery := DefaultDiscoveryConfig()
|
||||||
|
return &SystemSettings{
|
||||||
|
PBSPollingInterval: 60,
|
||||||
|
PMGPollingInterval: 60,
|
||||||
|
AutoUpdateEnabled: false,
|
||||||
|
DiscoveryEnabled: true,
|
||||||
|
DiscoverySubnet: "auto",
|
||||||
|
DiscoveryConfig: defaultDiscovery,
|
||||||
|
AllowEmbedding: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SaveNodesConfig saves nodes configuration to file (encrypted)
|
// SaveNodesConfig saves nodes configuration to file (encrypted)
|
||||||
func (c *ConfigPersistence) SaveNodesConfig(pveInstances []PVEInstance, pbsInstances []PBSInstance, pmgInstances []PMGInstance) error {
|
func (c *ConfigPersistence) SaveNodesConfig(pveInstances []PVEInstance, pbsInstances []PBSInstance, pmgInstances []PMGInstance) error {
|
||||||
return c.saveNodesConfig(pveInstances, pbsInstances, pmgInstances, false)
|
return c.saveNodesConfig(pveInstances, pbsInstances, pmgInstances, false)
|
||||||
|
|
@ -1154,13 +1169,16 @@ func (c *ConfigPersistence) LoadSystemSettings() (*SystemSettings, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var settings SystemSettings
|
settings := DefaultSystemSettings()
|
||||||
if err := json.Unmarshal(data, &settings); err != nil {
|
if settings == nil {
|
||||||
|
settings = &SystemSettings{}
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, settings); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("file", c.systemFile).Msg("System settings loaded")
|
log.Info().Str("file", c.systemFile).Msg("System settings loaded")
|
||||||
return &settings, nil
|
return settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateEnvFile updates the .env file with new system settings
|
// updateEnvFile updates the .env file with new system settings
|
||||||
|
|
|
||||||
180
internal/discovery/config_override.go
Normal file
180
internal/discovery/config_override.go
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||||
|
pkgdiscovery "github.com/rcourtman/pulse-go-rewrite/pkg/discovery"
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/pkg/discovery/envdetect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BuildScanner creates a discovery scanner configured using the supplied discovery config.
|
||||||
|
func BuildScanner(cfg config.DiscoveryConfig) (*pkgdiscovery.Scanner, error) {
|
||||||
|
profile, err := envdetect.DetectEnvironment()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyConfigToProfile(profile, cfg)
|
||||||
|
return pkgdiscovery.NewScannerWithProfile(profile), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyConfigToProfile mutates the supplied environment profile according to the discovery config.
|
||||||
|
func ApplyConfigToProfile(profile *envdetect.EnvironmentProfile, cfg config.DiscoveryConfig) {
|
||||||
|
if profile == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment override
|
||||||
|
if env, ok := environmentFromOverride(cfg.EnvironmentOverride); ok {
|
||||||
|
profile.Type = env
|
||||||
|
filterPhasesForEnvironment(profile, env)
|
||||||
|
} else if cfg.EnvironmentOverride != "" && strings.ToLower(cfg.EnvironmentOverride) != "auto" {
|
||||||
|
profile.Warnings = append(profile.Warnings, fmt.Sprintf("Unknown environment override: %s", cfg.EnvironmentOverride))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply subnet blocklist
|
||||||
|
blocked := parseCIDRMap(cfg.SubnetBlocklist, &profile.Warnings)
|
||||||
|
if len(blocked) > 0 {
|
||||||
|
var filtered []envdetect.SubnetPhase
|
||||||
|
for _, phase := range profile.Phases {
|
||||||
|
var kept []net.IPNet
|
||||||
|
for _, subnet := range phase.Subnets {
|
||||||
|
if _, blocked := blocked[subnet.String()]; blocked {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kept = append(kept, subnet)
|
||||||
|
}
|
||||||
|
if len(kept) > 0 {
|
||||||
|
phase.Subnets = kept
|
||||||
|
filtered = append(filtered, phase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profile.Phases = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply subnet allowlist as highest priority phase
|
||||||
|
if len(cfg.SubnetAllowlist) > 0 {
|
||||||
|
allowlist := parseCIDRs(cfg.SubnetAllowlist, &profile.Warnings)
|
||||||
|
if len(allowlist) > 0 {
|
||||||
|
if len(blocked) > 0 {
|
||||||
|
filtered := allowlist[:0]
|
||||||
|
for _, subnet := range allowlist {
|
||||||
|
if _, skip := blocked[subnet.String()]; skip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, subnet)
|
||||||
|
}
|
||||||
|
allowlist = filtered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(allowlist) > 0 {
|
||||||
|
allowPhase := envdetect.SubnetPhase{
|
||||||
|
Name: "config_allowlist",
|
||||||
|
Subnets: allowlist,
|
||||||
|
Confidence: 1.0,
|
||||||
|
Priority: 0,
|
||||||
|
}
|
||||||
|
profile.Phases = append([]envdetect.SubnetPhase{allowPhase}, profile.Phases...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override scan policy
|
||||||
|
if cfg.MaxHostsPerScan > 0 {
|
||||||
|
profile.Policy.MaxHostsPerScan = cfg.MaxHostsPerScan
|
||||||
|
}
|
||||||
|
if cfg.MaxConcurrent > 0 {
|
||||||
|
profile.Policy.MaxConcurrent = cfg.MaxConcurrent
|
||||||
|
}
|
||||||
|
profile.Policy.EnableReverseDNS = cfg.EnableReverseDNS
|
||||||
|
profile.Policy.ScanGateways = cfg.ScanGateways
|
||||||
|
|
||||||
|
if cfg.DialTimeout > 0 {
|
||||||
|
profile.Policy.DialTimeout = time.Duration(cfg.DialTimeout) * time.Millisecond
|
||||||
|
}
|
||||||
|
if cfg.HTTPTimeout > 0 {
|
||||||
|
profile.Policy.HTTPTimeout = time.Duration(cfg.HTTPTimeout) * time.Millisecond
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCIDRs(values []string, warnings *[]string) []net.IPNet {
|
||||||
|
var subnets []net.IPNet
|
||||||
|
for _, value := range values {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, ipNet, err := net.ParseCIDR(value)
|
||||||
|
if err != nil {
|
||||||
|
if warnings != nil {
|
||||||
|
*warnings = append(*warnings, fmt.Sprintf("Invalid CIDR '%s' ignored", value))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
subnets = append(subnets, *ipNet)
|
||||||
|
}
|
||||||
|
return subnets
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCIDRMap(values []string, warnings *[]string) map[string]struct{} {
|
||||||
|
cidrs := parseCIDRs(values, warnings)
|
||||||
|
result := make(map[string]struct{}, len(cidrs))
|
||||||
|
for _, cidr := range cidrs {
|
||||||
|
result[cidr.String()] = struct{}{}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func environmentFromOverride(value string) (envdetect.Environment, bool) {
|
||||||
|
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||||
|
switch normalized {
|
||||||
|
case "", "auto":
|
||||||
|
return envdetect.Unknown, false
|
||||||
|
case "native":
|
||||||
|
return envdetect.Native, true
|
||||||
|
case "docker_host":
|
||||||
|
return envdetect.DockerHost, true
|
||||||
|
case "docker_bridge":
|
||||||
|
return envdetect.DockerBridge, true
|
||||||
|
case "lxc_privileged":
|
||||||
|
return envdetect.LXCPrivileged, true
|
||||||
|
case "lxc_unprivileged":
|
||||||
|
return envdetect.LXCUnprivileged, true
|
||||||
|
default:
|
||||||
|
return envdetect.Unknown, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterPhasesForEnvironment(profile *envdetect.EnvironmentProfile, env envdetect.Environment) {
|
||||||
|
if len(profile.Phases) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var keep []envdetect.SubnetPhase
|
||||||
|
for _, phase := range profile.Phases {
|
||||||
|
name := strings.ToLower(phase.Name)
|
||||||
|
switch env {
|
||||||
|
case envdetect.Native, envdetect.DockerHost, envdetect.LXCPrivileged:
|
||||||
|
if strings.Contains(name, "local") || strings.Contains(name, "host") {
|
||||||
|
keep = append(keep, phase)
|
||||||
|
}
|
||||||
|
case envdetect.DockerBridge:
|
||||||
|
if strings.Contains(name, "container") || strings.Contains(name, "inferred") || strings.Contains(name, "host") {
|
||||||
|
keep = append(keep, phase)
|
||||||
|
}
|
||||||
|
case envdetect.LXCUnprivileged:
|
||||||
|
if strings.Contains(name, "lxc") || strings.Contains(name, "container") || strings.Contains(name, "parent") {
|
||||||
|
keep = append(keep, phase)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
keep = append(keep, phase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(keep) > 0 {
|
||||||
|
profile.Phases = keep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,14 +6,15 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/pkg/discovery"
|
pkgdiscovery "github.com/rcourtman/pulse-go-rewrite/pkg/discovery"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service handles background network discovery
|
// Service handles background network discovery
|
||||||
type Service struct {
|
type Service struct {
|
||||||
scanner *discovery.Scanner
|
scanner *pkgdiscovery.Scanner
|
||||||
wsHub *websocket.Hub
|
wsHub *websocket.Hub
|
||||||
cache *DiscoveryCache
|
cache *DiscoveryCache
|
||||||
interval time.Duration
|
interval time.Duration
|
||||||
|
|
@ -23,17 +24,18 @@ type Service struct {
|
||||||
isScanning bool
|
isScanning bool
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
cfgProvider func() config.DiscoveryConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiscoveryCache stores the latest discovery results
|
// DiscoveryCache stores the latest discovery results
|
||||||
type DiscoveryCache struct {
|
type DiscoveryCache struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
result *discovery.DiscoveryResult
|
result *pkgdiscovery.DiscoveryResult
|
||||||
updated time.Time
|
updated time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new discovery service
|
// NewService creates a new discovery service
|
||||||
func NewService(wsHub *websocket.Hub, interval time.Duration, subnet string) *Service {
|
func NewService(wsHub *websocket.Hub, interval time.Duration, subnet string, cfgProvider func() config.DiscoveryConfig) *Service {
|
||||||
if interval == 0 {
|
if interval == 0 {
|
||||||
interval = 5 * time.Minute // Default to 5 minutes
|
interval = 5 * time.Minute // Default to 5 minutes
|
||||||
}
|
}
|
||||||
|
|
@ -41,13 +43,18 @@ func NewService(wsHub *websocket.Hub, interval time.Duration, subnet string) *Se
|
||||||
subnet = "auto"
|
subnet = "auto"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfgProvider == nil {
|
||||||
|
cfgProvider = func() config.DiscoveryConfig { return config.DefaultDiscoveryConfig() }
|
||||||
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
scanner: discovery.NewScanner(),
|
scanner: pkgdiscovery.NewScanner(),
|
||||||
wsHub: wsHub,
|
wsHub: wsHub,
|
||||||
cache: &DiscoveryCache{},
|
cache: &DiscoveryCache{},
|
||||||
interval: interval,
|
interval: interval,
|
||||||
subnet: subnet,
|
subnet: subnet,
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
|
cfgProvider: cfgProvider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,7 +108,7 @@ func (s *Service) performScan() {
|
||||||
s.isScanning = true
|
s.isScanning = true
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
var result *discovery.DiscoveryResult
|
var result *pkgdiscovery.DiscoveryResult
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
|
|
@ -144,14 +151,22 @@ func (s *Service) performScan() {
|
||||||
scanCtx, cancel := context.WithTimeout(s.ctx, 2*time.Minute)
|
scanCtx, cancel := context.WithTimeout(s.ctx, 2*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Refresh scanner to ensure environment detection stays current
|
cfg := config.DefaultDiscoveryConfig()
|
||||||
newScanner := discovery.NewScanner()
|
if s.cfgProvider != nil {
|
||||||
|
cfg = config.CloneDiscoveryConfig(s.cfgProvider())
|
||||||
|
}
|
||||||
|
|
||||||
|
newScanner, err := BuildScanner(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Environment detection failed during discovery; falling back to default scanner configuration")
|
||||||
|
newScanner = pkgdiscovery.NewScanner()
|
||||||
|
}
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.scanner = newScanner
|
s.scanner = newScanner
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
// Perform the scan with real-time callback
|
// Perform the scan with real-time callback
|
||||||
result, err = newScanner.DiscoverServersWithCallback(scanCtx, s.subnet, func(server discovery.DiscoveredServer, phase string) {
|
result, err = newScanner.DiscoverServersWithCallback(scanCtx, s.subnet, func(server pkgdiscovery.DiscoveredServer, phase string) {
|
||||||
// Send immediate update for each discovered server
|
// Send immediate update for each discovered server
|
||||||
if s.wsHub != nil {
|
if s.wsHub != nil {
|
||||||
s.wsHub.Broadcast(websocket.Message{
|
s.wsHub.Broadcast(websocket.Message{
|
||||||
|
|
@ -221,15 +236,15 @@ func (s *Service) performScan() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCachedResult returns the cached discovery result
|
// GetCachedResult returns the cached discovery result
|
||||||
func (s *Service) GetCachedResult() (*discovery.DiscoveryResult, time.Time) {
|
func (s *Service) GetCachedResult() (*pkgdiscovery.DiscoveryResult, time.Time) {
|
||||||
s.cache.mu.RLock()
|
s.cache.mu.RLock()
|
||||||
defer s.cache.mu.RUnlock()
|
defer s.cache.mu.RUnlock()
|
||||||
|
|
||||||
if s.cache.result == nil {
|
if s.cache.result == nil {
|
||||||
return &discovery.DiscoveryResult{
|
return &pkgdiscovery.DiscoveryResult{
|
||||||
Servers: []discovery.DiscoveredServer{},
|
Servers: []pkgdiscovery.DiscoveredServer{},
|
||||||
Errors: []string{},
|
Errors: []string{},
|
||||||
}, time.Time{}
|
}, time.Time{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.cache.result, s.cache.updated
|
return s.cache.result, s.cache.updated
|
||||||
|
|
|
||||||
|
|
@ -1915,7 +1915,15 @@ func (m *Monitor) Start(ctx context.Context, wsHub *websocket.Hub) {
|
||||||
if discoverySubnet == "" {
|
if discoverySubnet == "" {
|
||||||
discoverySubnet = "auto"
|
discoverySubnet = "auto"
|
||||||
}
|
}
|
||||||
m.discoveryService = discovery.NewService(wsHub, 5*time.Minute, discoverySubnet)
|
cfgProvider := func() config.DiscoveryConfig {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
if m.config == nil {
|
||||||
|
return config.DefaultDiscoveryConfig()
|
||||||
|
}
|
||||||
|
return config.CloneDiscoveryConfig(m.config.Discovery)
|
||||||
|
}
|
||||||
|
m.discoveryService = discovery.NewService(wsHub, 5*time.Minute, discoverySubnet, cfgProvider)
|
||||||
if m.discoveryService != nil {
|
if m.discoveryService != nil {
|
||||||
m.discoveryService.Start(ctx)
|
m.discoveryService.Start(ctx)
|
||||||
log.Info().Msg("Discovery service initialized and started")
|
log.Info().Msg("Discovery service initialized and started")
|
||||||
|
|
@ -5444,7 +5452,16 @@ func (m *Monitor) StartDiscoveryService(ctx context.Context, wsHub *websocket.Hu
|
||||||
subnet = "auto"
|
subnet = "auto"
|
||||||
}
|
}
|
||||||
|
|
||||||
m.discoveryService = discovery.NewService(wsHub, 5*time.Minute, subnet)
|
cfgProvider := func() config.DiscoveryConfig {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
if m.config == nil {
|
||||||
|
return config.DefaultDiscoveryConfig()
|
||||||
|
}
|
||||||
|
return config.CloneDiscoveryConfig(m.config.Discovery)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.discoveryService = discovery.NewService(wsHub, 5*time.Minute, subnet, cfgProvider)
|
||||||
if m.discoveryService != nil {
|
if m.discoveryService != nil {
|
||||||
m.discoveryService.Start(ctx)
|
m.discoveryService.Start(ctx)
|
||||||
log.Info().Str("subnet", subnet).Msg("Discovery service started")
|
log.Info().Str("subnet", subnet).Msg("Discovery service started")
|
||||||
|
|
|
||||||
|
|
@ -423,10 +423,6 @@ func buildEnvironmentInfo(profile *envdetect.EnvironmentProfile) *EnvironmentInf
|
||||||
func ensurePolicyDefaults(policy envdetect.ScanPolicy) envdetect.ScanPolicy {
|
func ensurePolicyDefaults(policy envdetect.ScanPolicy) envdetect.ScanPolicy {
|
||||||
defaults := envdetect.DefaultScanPolicy()
|
defaults := envdetect.DefaultScanPolicy()
|
||||||
|
|
||||||
if policy == (envdetect.ScanPolicy{}) {
|
|
||||||
return defaults
|
|
||||||
}
|
|
||||||
|
|
||||||
if policy.MaxConcurrent <= 0 {
|
if policy.MaxConcurrent <= 0 {
|
||||||
policy.MaxConcurrent = defaults.MaxConcurrent
|
policy.MaxConcurrent = defaults.MaxConcurrent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue