feat: implement atomic config management in sensor proxy
This commit is contained in:
parent
18b0323340
commit
17d55b8cf4
2 changed files with 269 additions and 22 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue