mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-02 05:20:18 +00:00
- Change default server listen addresses to empty string (listen on all interfaces including IPv6) - Add short hostname matching fallback in host lookup API to handle FQDN vs short name mismatches - Implement retry loop (30s) in both Windows and Linux/macOS installers for registration verification - Fix lint errors: remove unnecessary fmt.Sprintf and nil checks before len() This resolves the 'Installer could not yet confirm host registration with Pulse' warning by addressing timing issues, hostname matching, and network connectivity.
210 lines
5.9 KiB
Go
210 lines
5.9 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
|
agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// HostAgentHandlers manages ingest from the pulse-host-agent.
|
|
type HostAgentHandlers struct {
|
|
monitor *monitoring.Monitor
|
|
wsHub *websocket.Hub
|
|
}
|
|
|
|
// NewHostAgentHandlers constructs a new handler set for host agents.
|
|
func NewHostAgentHandlers(m *monitoring.Monitor, hub *websocket.Hub) *HostAgentHandlers {
|
|
return &HostAgentHandlers{monitor: m, wsHub: hub}
|
|
}
|
|
|
|
// SetMonitor updates the monitor reference for host agent handlers.
|
|
func (h *HostAgentHandlers) SetMonitor(m *monitoring.Monitor) {
|
|
h.monitor = m
|
|
}
|
|
|
|
// HandleReport ingests host agent reports.
|
|
func (h *HostAgentHandlers) HandleReport(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only POST is allowed", nil)
|
|
return
|
|
}
|
|
|
|
defer r.Body.Close()
|
|
|
|
var report agentshost.Report
|
|
if err := json.NewDecoder(r.Body).Decode(&report); err != nil {
|
|
writeErrorResponse(w, http.StatusBadRequest, "invalid_json", "Failed to decode request body", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if report.Timestamp.IsZero() {
|
|
report.Timestamp = time.Now().UTC()
|
|
}
|
|
|
|
tokenRecord := getAPITokenRecordFromRequest(r)
|
|
|
|
host, err := h.monitor.ApplyHostReport(report, tokenRecord)
|
|
if err != nil {
|
|
writeErrorResponse(w, http.StatusBadRequest, "invalid_report", err.Error(), nil)
|
|
return
|
|
}
|
|
|
|
log.Debug().
|
|
Str("hostId", host.ID).
|
|
Str("hostname", host.Hostname).
|
|
Str("platform", host.Platform).
|
|
Msg("Host agent report processed")
|
|
|
|
go h.wsHub.BroadcastState(h.monitor.GetState().ToFrontend())
|
|
|
|
resp := map[string]any{
|
|
"success": true,
|
|
"hostId": host.ID,
|
|
"lastSeen": host.LastSeen,
|
|
"platform": host.Platform,
|
|
"osName": host.OSName,
|
|
"osVersion": host.OSVersion,
|
|
}
|
|
|
|
if err := utils.WriteJSONResponse(w, resp); err != nil {
|
|
log.Error().Err(err).Msg("Failed to serialize host agent response")
|
|
}
|
|
}
|
|
|
|
// HandleLookup returns host registration details for installer validation.
|
|
func (h *HostAgentHandlers) HandleLookup(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only GET is allowed", nil)
|
|
return
|
|
}
|
|
|
|
id := strings.TrimSpace(r.URL.Query().Get("id"))
|
|
hostname := strings.TrimSpace(r.URL.Query().Get("hostname"))
|
|
|
|
if id == "" && hostname == "" {
|
|
writeErrorResponse(w, http.StatusBadRequest, "missing_lookup_param", "Provide either id or hostname to look up a host", nil)
|
|
return
|
|
}
|
|
|
|
state := h.monitor.GetState()
|
|
|
|
var (
|
|
host models.Host
|
|
found bool
|
|
)
|
|
|
|
if id != "" {
|
|
for _, candidate := range state.Hosts {
|
|
if candidate.ID == id {
|
|
host = candidate
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !found && hostname != "" {
|
|
// First pass: exact match (case-insensitive)
|
|
for _, candidate := range state.Hosts {
|
|
if strings.EqualFold(candidate.Hostname, hostname) || strings.EqualFold(candidate.DisplayName, hostname) {
|
|
host = candidate
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// Second pass: short hostname match (if exact match failed)
|
|
if !found {
|
|
// Helper to get short hostname (before first dot)
|
|
getShortName := func(h string) string {
|
|
if idx := strings.Index(h, "."); idx != -1 {
|
|
return h[:idx]
|
|
}
|
|
return h
|
|
}
|
|
|
|
shortLookup := getShortName(hostname)
|
|
for _, candidate := range state.Hosts {
|
|
if strings.EqualFold(getShortName(candidate.Hostname), shortLookup) {
|
|
host = candidate
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
writeErrorResponse(w, http.StatusNotFound, "host_not_found", "Host has not registered with Pulse yet", nil)
|
|
return
|
|
}
|
|
|
|
// Ensure the querying token matches the host (when applicable).
|
|
if record := getAPITokenRecordFromRequest(r); record != nil && host.TokenID != "" && host.TokenID != record.ID {
|
|
writeErrorResponse(w, http.StatusForbidden, "host_lookup_forbidden", "Host does not belong to this API token", nil)
|
|
return
|
|
}
|
|
|
|
connected := strings.EqualFold(host.Status, "online") ||
|
|
strings.EqualFold(host.Status, "running") ||
|
|
strings.EqualFold(host.Status, "healthy")
|
|
|
|
resp := map[string]any{
|
|
"success": true,
|
|
"host": map[string]any{
|
|
"id": host.ID,
|
|
"hostname": host.Hostname,
|
|
"displayName": host.DisplayName,
|
|
"status": host.Status,
|
|
"connected": connected,
|
|
"lastSeen": host.LastSeen,
|
|
"agentVersion": host.AgentVersion,
|
|
},
|
|
}
|
|
|
|
if err := utils.WriteJSONResponse(w, resp); err != nil {
|
|
log.Error().Err(err).Msg("Failed to serialize host lookup response")
|
|
}
|
|
}
|
|
|
|
// HandleDeleteHost removes a host from the shared state.
|
|
func (h *HostAgentHandlers) HandleDeleteHost(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodDelete {
|
|
writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only DELETE is allowed", nil)
|
|
return
|
|
}
|
|
|
|
// Extract host ID from URL path
|
|
// Expected format: /api/agents/host/{hostId}
|
|
trimmedPath := strings.TrimPrefix(r.URL.Path, "/api/agents/host/")
|
|
hostID := strings.TrimSpace(trimmedPath)
|
|
if hostID == "" {
|
|
writeErrorResponse(w, http.StatusBadRequest, "missing_host_id", "Host ID is required", nil)
|
|
return
|
|
}
|
|
|
|
// Remove the host from state
|
|
host, err := h.monitor.RemoveHostAgent(hostID)
|
|
if err != nil {
|
|
writeErrorResponse(w, http.StatusNotFound, "host_not_found", err.Error(), nil)
|
|
return
|
|
}
|
|
|
|
go h.wsHub.BroadcastState(h.monitor.GetState().ToFrontend())
|
|
|
|
if err := utils.WriteJSONResponse(w, map[string]any{
|
|
"success": true,
|
|
"hostId": host.ID,
|
|
"message": "Host removed",
|
|
}); err != nil {
|
|
log.Error().Err(err).Msg("Failed to serialize host removal response")
|
|
}
|
|
}
|