feat: implement atomic config management in sensor proxy

This commit is contained in:
courtmanr@gmail.com 2025-11-20 19:01:24 +00:00
parent 18b0323340
commit 17d55b8cf4
2 changed files with 269 additions and 22 deletions

View file

@ -12,10 +12,21 @@ import (
var (
// Config command flags
configPathFlag string
allowedNodesPathFlag string
mergeNodesFlag []string
replaceMode bool
configPathFlag string
allowedNodesPathFlag string
mergeNodesFlag []string
replaceMode bool
// New flags for extended commands
controlPlaneURL string
controlPlaneToken string
controlPlaneRefresh int
httpEnabled bool
httpAddr string
httpAuthToken string
httpTLSCert string
httpTLSKey string
)
var configCmd = &cobra.Command{
@ -139,11 +150,114 @@ Safe to run multiple times (idempotent).
},
}
var addSubnetCmd = &cobra.Command{
Use: "add-subnet [subnet]",
Short: "Add an allowed source subnet",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
subnet := args[0]
cfgPath := configPathFlag
if cfgPath == "" {
cfgPath = defaultConfigPath
}
return updateConfigMap(cfgPath, func(config map[string]interface{}) error {
var subnets []string
if existing, ok := config["allowed_source_subnets"]; ok {
if list, ok := existing.([]interface{}); ok {
for _, item := range list {
if s, ok := item.(string); ok {
subnets = append(subnets, s)
}
}
}
}
// Check if already exists
for _, s := range subnets {
if s == subnet {
return nil // Already exists
}
}
subnets = append(subnets, subnet)
config["allowed_source_subnets"] = subnets
return nil
})
},
}
var setControlPlaneCmd = &cobra.Command{
Use: "set-control-plane",
Short: "Configure Pulse control plane connection",
RunE: func(cmd *cobra.Command, args []string) error {
cfgPath := configPathFlag
if cfgPath == "" {
cfgPath = defaultConfigPath
}
return updateConfigMap(cfgPath, func(config map[string]interface{}) error {
cp := make(map[string]interface{})
if existing, ok := config["pulse_control_plane"]; ok {
if m, ok := existing.(map[string]interface{}); ok {
cp = m
}
}
if controlPlaneURL != "" {
cp["url"] = controlPlaneURL
}
if controlPlaneToken != "" {
cp["token_file"] = controlPlaneToken
}
if controlPlaneRefresh > 0 {
cp["refresh_interval"] = controlPlaneRefresh
}
config["pulse_control_plane"] = cp
return nil
})
},
}
var setHTTPCmd = &cobra.Command{
Use: "set-http",
Short: "Configure HTTP mode settings",
RunE: func(cmd *cobra.Command, args []string) error {
cfgPath := configPathFlag
if cfgPath == "" {
cfgPath = defaultConfigPath
}
return updateConfigMap(cfgPath, func(config map[string]interface{}) error {
if cmd.Flags().Changed("enabled") {
config["http_enabled"] = httpEnabled
}
if httpAddr != "" {
config["http_listen_addr"] = httpAddr
}
if httpAuthToken != "" {
config["http_auth_token"] = httpAuthToken
}
if httpTLSCert != "" {
config["http_tls_cert"] = httpTLSCert
}
if httpTLSKey != "" {
config["http_tls_key"] = httpTLSKey
}
return nil
})
},
}
func init() {
// Add subcommands to config command
configCmd.AddCommand(validateCmd)
configCmd.AddCommand(setAllowedNodesCmd)
configCmd.AddCommand(migrateToFileCmd)
configCmd.AddCommand(addSubnetCmd)
configCmd.AddCommand(setControlPlaneCmd)
configCmd.AddCommand(setHTTPCmd)
// Validate command flags
validateCmd.Flags().StringVar(&configPathFlag, "config", "", "Path to config.yaml (default: /etc/pulse-sensor-proxy/config.yaml)")
@ -158,10 +272,72 @@ func init() {
migrateToFileCmd.Flags().StringVar(&configPathFlag, "config", "", "Path to config.yaml (default: /etc/pulse-sensor-proxy/config.yaml)")
migrateToFileCmd.Flags().StringVar(&allowedNodesPathFlag, "allowed-nodes", "", "Path to allowed_nodes.yaml (default: same dir as config)")
// Add-subnet command flags
addSubnetCmd.Flags().StringVar(&configPathFlag, "config", "", "Path to config.yaml")
// Set-control-plane command flags
setControlPlaneCmd.Flags().StringVar(&configPathFlag, "config", "", "Path to config.yaml")
setControlPlaneCmd.Flags().StringVar(&controlPlaneURL, "url", "", "Control plane URL")
setControlPlaneCmd.Flags().StringVar(&controlPlaneToken, "token-file", "", "Path to token file")
setControlPlaneCmd.Flags().IntVar(&controlPlaneRefresh, "refresh", 0, "Refresh interval in seconds")
// Set-http command flags
setHTTPCmd.Flags().StringVar(&configPathFlag, "config", "", "Path to config.yaml")
setHTTPCmd.Flags().BoolVar(&httpEnabled, "enabled", false, "Enable HTTP mode")
setHTTPCmd.Flags().StringVar(&httpAddr, "listen-addr", "", "HTTP listen address")
setHTTPCmd.Flags().StringVar(&httpAuthToken, "auth-token", "", "HTTP auth token")
setHTTPCmd.Flags().StringVar(&httpTLSCert, "tls-cert", "", "TLS certificate path")
setHTTPCmd.Flags().StringVar(&httpTLSKey, "tls-key", "", "TLS key path")
// Add config command to root
rootCmd.AddCommand(configCmd)
}
// updateConfigMap safely updates the config file using a map representation
func updateConfigMap(path string, updateFn func(map[string]interface{}) error) error {
lockPath := path + ".lock"
return withLockedFile(lockPath, func(f *os.File) error {
// Read current config
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
data = []byte("{}\n")
} else {
return fmt.Errorf("failed to read config: %w", err)
}
}
// Sanitize duplicate blocks before parsing
_, data = sanitizeDuplicateAllowedNodesBlocks(path, data)
var config map[string]interface{}
if err := yaml.Unmarshal(data, &config); err != nil {
return fmt.Errorf("failed to parse config: %w", err)
}
if config == nil {
config = make(map[string]interface{})
}
// Apply updates
if err := updateFn(config); err != nil {
return err
}
// Write back
newData, err := yaml.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
// Preserve header comment if possible (simple heuristic)
header := "# Managed by pulse-sensor-proxy config CLI\n"
finalData := []byte(header + string(newData))
return atomicWriteFile(path, finalData, 0644)
})
}
// validateConfigFile parses and validates the main config file
func validateConfigFile(path string) error {
data, err := os.ReadFile(path)

View file

@ -154,6 +154,16 @@ ensure_allowed_source_subnet() {
return
fi
# Use robust binary config management if available
if [[ -x "$BINARY_PATH" ]] && "$BINARY_PATH" config add-subnet --help >/dev/null 2>&1; then
if "$BINARY_PATH" config add-subnet "$subnet" --config "$CONFIG_FILE"; then
print_info "Added allowed_source_subnets entry ${subnet}"
return
else
print_warn "Failed to add subnet using binary; falling back to legacy method"
fi
fi
local escaped_subnet="${subnet//\//\\/}"
if grep -Eq "^[[:space:]]+-[[:space:]]*${escaped_subnet}([[:space:]]|$)" "$CONFIG_FILE"; then
return
@ -163,16 +173,24 @@ ensure_allowed_source_subnet() {
tmp=$(mktemp)
if grep -Eq "^[[:space:]]*allowed_source_subnets:" "$CONFIG_FILE"; then
awk -v subnet="$subnet" '
/^allowed_source_subnets:/ {print; in_block=1; next}
awk -v subnet="$subnet" '
/^allowed_source_subnets:/ {print; in_block=1; indent=" "; next}
in_block && /^[[:space:]]+-/ {
# Capture indentation from existing items
if (!captured) {
match($0, /^[[:space:]]+/)
indent = substr($0, RSTART, RLENGTH)
captured = 1
}
}
in_block && /^[^[:space:]]/ {
if (!added) { printf(" - %s\n", subnet); added=1 }
if (!added) { printf("%s- %s\n", indent, subnet); added=1 }
in_block=0
}
{print}
END {
if (in_block && !added) {
printf(" - %s\n", subnet)
printf("%s- %s\n", indent, subnet)
}
}
' "$CONFIG_FILE" > "$tmp"
@ -181,7 +199,7 @@ END {
{
echo ""
echo "allowed_source_subnets:"
echo " - $subnet"
echo " - $subnet"
} >> "$tmp"
fi
@ -1659,6 +1677,17 @@ ensure_control_plane_config() {
refresh=60
fi
# Use robust binary config management if available
if [[ -x "$BINARY_PATH" ]] && "$BINARY_PATH" config set-control-plane --help >/dev/null 2>&1; then
if "$BINARY_PATH" config set-control-plane --url "$pulse_url" --token-file "/etc/pulse-sensor-proxy/.pulse-control-token" --refresh "$refresh" --config "$config_file"; then
chown pulse-sensor-proxy:pulse-sensor-proxy "$config_file"
chmod 0644 "$config_file"
return
else
print_warn "Failed to set control plane using binary; falling back to legacy method"
fi
fi
if grep -q "^pulse_control_plane:" "$config_file" 2>/dev/null; then
# Re-write the existing control-plane block with the latest URL/token path.
local tmp
@ -1970,16 +1999,30 @@ if [[ "$HTTP_MODE" == true ]]; then
# Configure HTTP mode - check if already configured to avoid duplicates
print_info "Configuring HTTP mode..."
if grep -q "^http_enabled:" "$CONFIG_FILE" 2>/dev/null; then
# HTTP mode already configured - only update the token (avoid duplicates)
sed -i "s|^http_auth_token:.*|http_auth_token: $HTTP_AUTH_TOKEN|" "$CONFIG_FILE"
for subnet in "${HTTP_ALLOWED_SUBNETS[@]}"; do
ensure_allowed_source_subnet "$subnet"
done
print_info "Updated HTTP auth token (existing HTTP mode configuration kept)"
else
# Fresh HTTP mode configuration - append to file
cat >> "$CONFIG_FILE" << EOF
if [[ -x "$BINARY_PATH" ]] && "$BINARY_PATH" config set-http --help >/dev/null 2>&1; then
if "$BINARY_PATH" config set-http \
--enabled=true \
--listen-addr="$HTTP_ADDR" \
--auth-token="$HTTP_AUTH_TOKEN" \
--tls-cert="/etc/pulse-sensor-proxy/tls/server.crt" \
--tls-key="/etc/pulse-sensor-proxy/tls/server.key" \
--config="$CONFIG_FILE"; then
for subnet in "${HTTP_ALLOWED_SUBNETS[@]}"; do
ensure_allowed_source_subnet "$subnet"
done
print_info "HTTP mode configured successfully (using binary)"
else
print_warn "Failed to set HTTP config using binary; falling back to legacy method"
# Fallback to legacy logic
if grep -q "^http_enabled:" "$CONFIG_FILE" 2>/dev/null; then
sed -i "s|^http_auth_token:.*|http_auth_token: $HTTP_AUTH_TOKEN|" "$CONFIG_FILE"
for subnet in "${HTTP_ALLOWED_SUBNETS[@]}"; do
ensure_allowed_source_subnet "$subnet"
done
print_info "Updated HTTP auth token (existing HTTP mode configuration kept)"
else
cat >> "$CONFIG_FILE" << EOF
# HTTP Mode Configuration (External PVE Host)
http_enabled: true
@ -1991,9 +2034,37 @@ http_auth_token: "$HTTP_AUTH_TOKEN"
# Allow HTTP connections from Pulse server, localhost, and this host
allowed_source_subnets:
EOF
for subnet in "${HTTP_ALLOWED_SUBNETS[@]}"; do
echo " - $subnet" >> "$CONFIG_FILE"
done
for subnet in "${HTTP_ALLOWED_SUBNETS[@]}"; do
echo " - $subnet" >> "$CONFIG_FILE"
done
fi
fi
else
if grep -q "^http_enabled:" "$CONFIG_FILE" 2>/dev/null; then
# HTTP mode already configured - only update the token (avoid duplicates)
sed -i "s|^http_auth_token:.*|http_auth_token: $HTTP_AUTH_TOKEN|" "$CONFIG_FILE"
for subnet in "${HTTP_ALLOWED_SUBNETS[@]}"; do
ensure_allowed_source_subnet "$subnet"
done
print_info "Updated HTTP auth token (existing HTTP mode configuration kept)"
else
# Fresh HTTP mode configuration - append to file
cat >> "$CONFIG_FILE" << EOF
# HTTP Mode Configuration (External PVE Host)
http_enabled: true
http_listen_addr: "$HTTP_ADDR"
http_tls_cert: /etc/pulse-sensor-proxy/tls/server.crt
http_tls_key: /etc/pulse-sensor-proxy/tls/server.key
http_auth_token: "$HTTP_AUTH_TOKEN"
# Allow HTTP connections from Pulse server, localhost, and this host
allowed_source_subnets:
EOF
for subnet in "${HTTP_ALLOWED_SUBNETS[@]}"; do
echo " - $subnet" >> "$CONFIG_FILE"
done
fi
fi
chown pulse-sensor-proxy:pulse-sensor-proxy "$CONFIG_FILE"
chmod 0644 "$CONFIG_FILE"