Pulse/internal/tempproxy/client.go
rcourtman 123e0f04ca feat: add comprehensive node cleanup system
Implements automated cleanup workflow when nodes are deleted from Pulse, removing all monitoring footprint from the host. Changes include a new RPC handler in the sensor proxy for cleanup requests, enhanced node deletion modal with detailed cleanup explanations, and improved SSH key management with proper tagging for atomic updates.
2025-10-17 18:53:45 +00:00

192 lines
4.5 KiB
Go

package tempproxy
import (
"encoding/json"
"fmt"
"net"
"os"
"time"
)
const (
defaultSocketPath = "/run/pulse-sensor-proxy/pulse-sensor-proxy.sock"
containerSocketPath = "/mnt/pulse-proxy/pulse-sensor-proxy.sock"
defaultTimeout = 30 * time.Second // Increased to accommodate SSH operations
)
// Client communicates with pulse-sensor-proxy via unix socket
type Client struct {
socketPath string
timeout time.Duration
}
// NewClient creates a new proxy client
func NewClient() *Client {
socketPath := os.Getenv("PULSE_SENSOR_PROXY_SOCKET")
if socketPath == "" {
if _, err := os.Stat(defaultSocketPath); err == nil {
socketPath = defaultSocketPath
} else if _, err := os.Stat(containerSocketPath); err == nil {
socketPath = containerSocketPath
} else {
socketPath = defaultSocketPath
}
}
return &Client{
socketPath: socketPath,
timeout: defaultTimeout,
}
}
// IsAvailable checks if the proxy is running and accessible
func (c *Client) IsAvailable() bool {
_, err := os.Stat(c.socketPath)
return err == nil
}
// RPCRequest represents a request to the proxy
type RPCRequest struct {
Method string `json:"method"`
Params map[string]interface{} `json:"params"`
}
// RPCResponse represents a response from the proxy
type RPCResponse struct {
Success bool `json:"success"`
Data map[string]interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
// call sends an RPC request and returns the response
func (c *Client) call(method string, params map[string]interface{}) (*RPCResponse, error) {
// Connect to unix socket
conn, err := net.DialTimeout("unix", c.socketPath, c.timeout)
if err != nil {
return nil, fmt.Errorf("failed to connect to proxy: %w", err)
}
defer conn.Close()
// Set deadline
conn.SetDeadline(time.Now().Add(c.timeout))
// Send request
req := RPCRequest{
Method: method,
Params: params,
}
encoder := json.NewEncoder(conn)
if err := encoder.Encode(req); err != nil {
return nil, fmt.Errorf("failed to encode request: %w", err)
}
// Read response (server uses newline-delimited framing, no CloseWrite needed)
var resp RPCResponse
decoder := json.NewDecoder(conn)
if err := decoder.Decode(&resp); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &resp, nil
}
// GetStatus returns proxy status
func (c *Client) GetStatus() (map[string]interface{}, error) {
resp, err := c.call("get_status", nil)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("proxy error: %s", resp.Error)
}
return resp.Data, nil
}
// RegisterNodes returns list of discovered nodes with SSH status
func (c *Client) RegisterNodes() ([]map[string]interface{}, error) {
resp, err := c.call("register_nodes", nil)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("proxy error: %s", resp.Error)
}
// Extract nodes array from data
nodesRaw, ok := resp.Data["nodes"]
if !ok {
return nil, fmt.Errorf("no nodes in response")
}
// Type assertion to []interface{} first, then convert
nodesArray, ok := nodesRaw.([]interface{})
if !ok {
return nil, fmt.Errorf("nodes is not an array")
}
nodes := make([]map[string]interface{}, len(nodesArray))
for i, nodeRaw := range nodesArray {
node, ok := nodeRaw.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("node %d is not a map", i)
}
nodes[i] = node
}
return nodes, nil
}
// GetTemperature fetches temperature data from a specific node
func (c *Client) GetTemperature(nodeHost string) (string, error) {
params := map[string]interface{}{
"node": nodeHost,
}
resp, err := c.call("get_temperature", params)
if err != nil {
return "", err
}
if !resp.Success {
return "", fmt.Errorf("proxy error: %s", resp.Error)
}
// Extract temperature JSON string
tempRaw, ok := resp.Data["temperature"]
if !ok {
return "", fmt.Errorf("no temperature data in response")
}
tempStr, ok := tempRaw.(string)
if !ok {
return "", fmt.Errorf("temperature is not a string")
}
return tempStr, nil
}
// RequestCleanup signals the proxy to trigger host-side cleanup workflow.
func (c *Client) RequestCleanup(host string) error {
params := make(map[string]interface{}, 1)
if host != "" {
params["host"] = host
}
resp, err := c.call("request_cleanup", params)
if err != nil {
return err
}
if !resp.Success {
if resp.Error != "" {
return fmt.Errorf("proxy error: %s", resp.Error)
}
return fmt.Errorf("proxy rejected cleanup request")
}
return nil
}