diff --git a/cmd/pulse-sensor-proxy/config.go b/cmd/pulse-sensor-proxy/config.go index 874461a..62e7092 100644 --- a/cmd/pulse-sensor-proxy/config.go +++ b/cmd/pulse-sensor-proxy/config.go @@ -1,6 +1,8 @@ package main import ( + "bufio" + "bytes" "fmt" "net" "os" @@ -24,6 +26,7 @@ type Config struct { MetricsAddress string `yaml:"metrics_address"` LogLevel string `yaml:"log_level"` AllowedNodes []string `yaml:"allowed_nodes"` + AllowedNodesFile string `yaml:"allowed_nodes_file"` StrictNodeValidation bool `yaml:"strict_node_validation"` ReadTimeout time.Duration `yaml:"read_timeout"` WriteTimeout time.Duration `yaml:"write_timeout"` @@ -55,6 +58,8 @@ type ControlPlaneConfig struct { InsecureSkipVerify bool `yaml:"insecure_skip_verify"` } +const defaultAllowedNodesFile = "/etc/pulse-sensor-proxy/allowed_nodes.yaml" + // PeerConfig represents a peer entry with capabilities. type PeerConfig struct { UID uint32 `yaml:"uid"` @@ -204,6 +209,29 @@ func loadConfig(configPath string) (*Config, error) { } } + if cfg.AllowedNodesFile == "" { + if _, err := os.Stat(defaultAllowedNodesFile); err == nil { + cfg.AllowedNodesFile = defaultAllowedNodesFile + } + } + + if cfg.AllowedNodesFile != "" { + if fileNodes, err := loadAllowedNodesFile(cfg.AllowedNodesFile); err != nil { + log.Warn(). + Err(err). + Str("allowed_nodes_file", cfg.AllowedNodesFile). + Msg("Failed to load allowed nodes file") + } else if len(fileNodes) > 0 { + cfg.AllowedNodes = append(cfg.AllowedNodes, fileNodes...) + log.Info(). + Str("allowed_nodes_file", cfg.AllowedNodesFile). + Int("allowed_node_count", len(fileNodes)). + Msg("Loaded allowed nodes from file") + } + } + + cfg.AllowedNodes = normalizeNodes(cfg.AllowedNodes) + // Strict node validation override if envStrict := os.Getenv("PULSE_SENSOR_PROXY_STRICT_NODE_VALIDATION"); envStrict != "" { parsed, err := parseBool(envStrict) @@ -386,6 +414,64 @@ func splitAndTrim(raw string) []string { return result } +func normalizeNodes(nodes []string) []string { + seen := make(map[string]struct{}) + var result []string + for _, node := range nodes { + trimmed := strings.TrimSpace(node) + if trimmed == "" { + continue + } + key := strings.ToLower(trimmed) + if _, exists := seen[key]; exists { + continue + } + seen[key] = struct{}{} + result = append(result, trimmed) + } + return result +} + +func loadAllowedNodesFile(path string) ([]string, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + type wrapper struct { + AllowedNodes []string `yaml:"allowed_nodes"` + } + + var w wrapper + if err := yaml.Unmarshal(data, &w); err == nil && len(w.AllowedNodes) > 0 { + return normalizeNodes(w.AllowedNodes), nil + } + + var list []string + if err := yaml.Unmarshal(data, &list); err == nil && len(list) > 0 { + return normalizeNodes(list), nil + } + + scanner := bufio.NewScanner(bytes.NewReader(data)) + var plain []string + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + if strings.HasPrefix(line, "-") { + line = strings.TrimSpace(strings.TrimPrefix(line, "-")) + } + if line != "" { + plain = append(plain, line) + } + } + if err := scanner.Err(); err != nil { + return normalizeNodes(plain), err + } + return normalizeNodes(plain), nil +} + // detectHostCIDRs detects local host IP addresses as /32 (IPv4) or /128 (IPv6) CIDRs func detectHostCIDRs() []string { var cidrs []string diff --git a/scripts/install-sensor-proxy.sh b/scripts/install-sensor-proxy.sh index b925da6..4703d87 100755 --- a/scripts/install-sensor-proxy.sh +++ b/scripts/install-sensor-proxy.sh @@ -6,6 +6,9 @@ set -euo pipefail +CONFIG_FILE="/etc/pulse-sensor-proxy/config.yaml" +ALLOWED_NODES_FILE="/etc/pulse-sensor-proxy/allowed_nodes.yaml" + # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' @@ -79,44 +82,27 @@ EOF fi } -# Helper function to safely update allowed_nodes section in config file -# Removes any existing allowed_nodes section before adding new one to prevent duplicates -update_allowed_nodes() { - local config_file="/etc/pulse-sensor-proxy/config.yaml" - local comment_line="$1" - shift - local nodes=("$@") - - mkdir -p "$(dirname "$config_file")" - touch "$config_file" - - # Gather existing nodes (if any) to preserve manual edits - local existing_nodes=() - if command -v perl >/dev/null 2>&1; then - mapfile -t existing_nodes < <(perl -0ne 'if (m/allowed_nodes:\n((?:[ \t]+-[^\n]*\n)+)/m) { @lines = split /\n/, $1; for (@lines) { s/^[ \t-]+//; s/\s+$//; next unless $_; print "$_\n"; } }' "$config_file" 2>/dev/null || true) +ensure_allowed_nodes_file_reference() { + if [[ ! -f "$CONFIG_FILE" ]]; then + return fi + if ! grep -q "allowed_nodes_file" "$CONFIG_FILE" 2>/dev/null; then + echo 'allowed_nodes_file: "/etc/pulse-sensor-proxy/allowed_nodes.yaml"' >> "$CONFIG_FILE" + fi +} - # Merge and de-duplicate nodes - local merged_nodes=() - declare -A seen_nodes=() - for node in "${existing_nodes[@]}" "${nodes[@]}"; do - node=$(echo "$node" | awk '{$1=$1;print}') - if [[ -z "$node" ]]; then - continue - fi - if [[ -z "${seen_nodes[$node]+set}" ]]; then - merged_nodes+=("$node") - seen_nodes["$node"]=1 - fi - done - - # Remove any existing allowed_nodes block (including descriptive comments) to prevent duplicates - if command -v python3 >/dev/null 2>&1; then - python3 - "$config_file" <<'PY' +remove_allowed_nodes_block() { + if ! command -v python3 >/dev/null 2>&1; then + sed -i '/allowed_nodes:/d' "$CONFIG_FILE" 2>/dev/null || true + return + fi + python3 - "$CONFIG_FILE" <<'PY' import sys from pathlib import Path path = Path(sys.argv[1]) +if not path.exists(): + sys.exit(0) lines = path.read_text().splitlines() out = [] pending = [] @@ -131,49 +117,93 @@ i = 0 while i < len(lines): line = lines[i] stripped = line.lstrip() - if skip: - # Keep skipping until a non-indented, non-comment line appears if stripped == '' or stripped.startswith('#') or stripped.startswith('-') or line.startswith((' ', '\t')): i += 1 continue skip = False - if stripped.startswith('allowed_nodes:'): pending.clear() skip = True i += 1 continue - if stripped == '' or stripped.startswith('#'): pending.append(line) i += 1 continue - flush_pending() out.append(line) i += 1 - flush_pending() path.write_text('\n'.join(out) + ('\n' if out else '')) PY - else - # Fallback: truncate the file to remove duplicates if python3 is unavailable - grep -v '^allowed_nodes:' "$config_file" > "${config_file}.tmp" && mv "${config_file}.tmp" "$config_file" +} + +update_allowed_nodes() { + local comment_line="$1" + shift + local nodes=("$@") + + ensure_allowed_nodes_file_reference + remove_allowed_nodes_block + + if ! command -v python3 >/dev/null 2>&1; then + print_warn "python3 is required to manage allowed_nodes; skipping update" + return fi - { - echo "" - echo "# ${comment_line}" - echo "# These nodes are allowed to request temperature data when cluster IPC validation is unavailable" - echo "allowed_nodes:" - for node in "${merged_nodes[@]}"; do - echo " - $node" - done - } >> "$config_file" + python3 - "$ALLOWED_NODES_FILE" "$comment_line" "${nodes[@]}" <<'PY' +import sys +from pathlib import Path +import yaml - chmod 0644 "$config_file" 2>/dev/null || true - chown pulse-sensor-proxy:pulse-sensor-proxy "$config_file" 2>/dev/null || true +path = Path(sys.argv[1]) +comment = sys.argv[2] +new_nodes = [n.strip() for n in sys.argv[3:] if n.strip()] +existing = [] +if path.exists(): + text = path.read_text() + try: + data = yaml.safe_load(text) + except yaml.YAMLError: + data = None + if isinstance(data, dict): + arr = data.get('allowed_nodes') + if isinstance(arr, list): + existing = [str(x).strip() for x in arr if str(x).strip()] + elif isinstance(data, list): + existing = [str(x).strip() for x in data if str(x).strip()] + else: + for line in text.splitlines(): + line = line.strip() + if not line or line.startswith('#'): + continue + if line.startswith('-'): + line = line[1:].strip() + if line: + existing.append(line) + +seen = set() +merged = [] +for entry in existing + new_nodes: + entry = entry.strip() + if not entry: + continue + key = entry.lower() + if key in seen: + continue + seen.add(key) + merged.append(entry) + +path.parent.mkdir(parents=True, exist_ok=True) +with path.open('w') as fh: + fh.write("# Managed by install-sensor-proxy.sh\n") + fh.write(f"# {comment}\n") + yaml.safe_dump({'allowed_nodes': merged}, fh, default_flow_style=False, sort_keys=False) +PY + + chmod 0644 "$ALLOWED_NODES_FILE" 2>/dev/null || true + chown pulse-sensor-proxy:pulse-sensor-proxy "$ALLOWED_NODES_FILE" 2>/dev/null || true } @@ -1125,6 +1155,8 @@ EOF chmod 0644 /etc/pulse-sensor-proxy/config.yaml fi +ensure_allowed_nodes_file_reference + # Register socket-mode proxy with Pulse if server provided if [[ "$HTTP_MODE" != true ]]; then if [[ -z "$PULSE_SERVER" ]]; then