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 (
|
var (
|
||||||
// Config command flags
|
// Config command flags
|
||||||
configPathFlag string
|
configPathFlag string
|
||||||
allowedNodesPathFlag string
|
allowedNodesPathFlag string
|
||||||
mergeNodesFlag []string
|
mergeNodesFlag []string
|
||||||
replaceMode bool
|
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{
|
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() {
|
func init() {
|
||||||
// Add subcommands to config command
|
// Add subcommands to config command
|
||||||
configCmd.AddCommand(validateCmd)
|
configCmd.AddCommand(validateCmd)
|
||||||
configCmd.AddCommand(setAllowedNodesCmd)
|
configCmd.AddCommand(setAllowedNodesCmd)
|
||||||
configCmd.AddCommand(migrateToFileCmd)
|
configCmd.AddCommand(migrateToFileCmd)
|
||||||
|
configCmd.AddCommand(addSubnetCmd)
|
||||||
|
configCmd.AddCommand(setControlPlaneCmd)
|
||||||
|
configCmd.AddCommand(setHTTPCmd)
|
||||||
|
|
||||||
// Validate command flags
|
// Validate command flags
|
||||||
validateCmd.Flags().StringVar(&configPathFlag, "config", "", "Path to config.yaml (default: /etc/pulse-sensor-proxy/config.yaml)")
|
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(&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)")
|
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
|
// Add config command to root
|
||||||
rootCmd.AddCommand(configCmd)
|
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
|
// validateConfigFile parses and validates the main config file
|
||||||
func validateConfigFile(path string) error {
|
func validateConfigFile(path string) error {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,16 @@ ensure_allowed_source_subnet() {
|
||||||
return
|
return
|
||||||
fi
|
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//\//\\/}"
|
local escaped_subnet="${subnet//\//\\/}"
|
||||||
if grep -Eq "^[[:space:]]+-[[:space:]]*${escaped_subnet}([[:space:]]|$)" "$CONFIG_FILE"; then
|
if grep -Eq "^[[:space:]]+-[[:space:]]*${escaped_subnet}([[:space:]]|$)" "$CONFIG_FILE"; then
|
||||||
return
|
return
|
||||||
|
|
@ -163,16 +173,24 @@ ensure_allowed_source_subnet() {
|
||||||
tmp=$(mktemp)
|
tmp=$(mktemp)
|
||||||
|
|
||||||
if grep -Eq "^[[:space:]]*allowed_source_subnets:" "$CONFIG_FILE"; then
|
if grep -Eq "^[[:space:]]*allowed_source_subnets:" "$CONFIG_FILE"; then
|
||||||
awk -v subnet="$subnet" '
|
awk -v subnet="$subnet" '
|
||||||
/^allowed_source_subnets:/ {print; in_block=1; next}
|
/^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:]]/ {
|
in_block && /^[^[:space:]]/ {
|
||||||
if (!added) { printf(" - %s\n", subnet); added=1 }
|
if (!added) { printf("%s- %s\n", indent, subnet); added=1 }
|
||||||
in_block=0
|
in_block=0
|
||||||
}
|
}
|
||||||
{print}
|
{print}
|
||||||
END {
|
END {
|
||||||
if (in_block && !added) {
|
if (in_block && !added) {
|
||||||
printf(" - %s\n", subnet)
|
printf("%s- %s\n", indent, subnet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
' "$CONFIG_FILE" > "$tmp"
|
' "$CONFIG_FILE" > "$tmp"
|
||||||
|
|
@ -181,7 +199,7 @@ END {
|
||||||
{
|
{
|
||||||
echo ""
|
echo ""
|
||||||
echo "allowed_source_subnets:"
|
echo "allowed_source_subnets:"
|
||||||
echo " - $subnet"
|
echo " - $subnet"
|
||||||
} >> "$tmp"
|
} >> "$tmp"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -1659,6 +1677,17 @@ ensure_control_plane_config() {
|
||||||
refresh=60
|
refresh=60
|
||||||
fi
|
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
|
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.
|
# Re-write the existing control-plane block with the latest URL/token path.
|
||||||
local tmp
|
local tmp
|
||||||
|
|
@ -1970,16 +1999,30 @@ if [[ "$HTTP_MODE" == true ]]; then
|
||||||
|
|
||||||
# Configure HTTP mode - check if already configured to avoid duplicates
|
# Configure HTTP mode - check if already configured to avoid duplicates
|
||||||
print_info "Configuring HTTP mode..."
|
print_info "Configuring HTTP mode..."
|
||||||
if grep -q "^http_enabled:" "$CONFIG_FILE" 2>/dev/null; then
|
if [[ -x "$BINARY_PATH" ]] && "$BINARY_PATH" config set-http --help >/dev/null 2>&1; then
|
||||||
# HTTP mode already configured - only update the token (avoid duplicates)
|
if "$BINARY_PATH" config set-http \
|
||||||
sed -i "s|^http_auth_token:.*|http_auth_token: $HTTP_AUTH_TOKEN|" "$CONFIG_FILE"
|
--enabled=true \
|
||||||
for subnet in "${HTTP_ALLOWED_SUBNETS[@]}"; do
|
--listen-addr="$HTTP_ADDR" \
|
||||||
ensure_allowed_source_subnet "$subnet"
|
--auth-token="$HTTP_AUTH_TOKEN" \
|
||||||
done
|
--tls-cert="/etc/pulse-sensor-proxy/tls/server.crt" \
|
||||||
print_info "Updated HTTP auth token (existing HTTP mode configuration kept)"
|
--tls-key="/etc/pulse-sensor-proxy/tls/server.key" \
|
||||||
else
|
--config="$CONFIG_FILE"; then
|
||||||
# Fresh HTTP mode configuration - append to file
|
|
||||||
cat >> "$CONFIG_FILE" << EOF
|
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 Mode Configuration (External PVE Host)
|
||||||
http_enabled: true
|
http_enabled: true
|
||||||
|
|
@ -1991,9 +2034,37 @@ http_auth_token: "$HTTP_AUTH_TOKEN"
|
||||||
# Allow HTTP connections from Pulse server, localhost, and this host
|
# Allow HTTP connections from Pulse server, localhost, and this host
|
||||||
allowed_source_subnets:
|
allowed_source_subnets:
|
||||||
EOF
|
EOF
|
||||||
for subnet in "${HTTP_ALLOWED_SUBNETS[@]}"; do
|
for subnet in "${HTTP_ALLOWED_SUBNETS[@]}"; do
|
||||||
echo " - $subnet" >> "$CONFIG_FILE"
|
echo " - $subnet" >> "$CONFIG_FILE"
|
||||||
done
|
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
|
fi
|
||||||
chown pulse-sensor-proxy:pulse-sensor-proxy "$CONFIG_FILE"
|
chown pulse-sensor-proxy:pulse-sensor-proxy "$CONFIG_FILE"
|
||||||
chmod 0644 "$CONFIG_FILE"
|
chmod 0644 "$CONFIG_FILE"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue