feat: notify server during agent uninstallation

- Add /api/agents/host/uninstall endpoint for agent self-unregistration
- Update install.sh to notify server during --uninstall (reads agent ID from disk)
- Update install.ps1 with same logic for Windows
- Update frontend uninstall command to include URL/token flags

This ensures that when an agent is uninstalled, the host record is
immediately removed from Pulse and any linked PVE nodes have their
+Agent badge cleared.
This commit is contained in:
rcourtman
2025-12-26 22:38:46 +00:00
parent 22d6c1d8a5
commit 8c440b6f54
5 changed files with 101 additions and 3 deletions
@@ -247,7 +247,9 @@ export const UnifiedAgents: Component = () => {
const getUninstallCommand = () => {
const url = customAgentUrl() || agentUrl();
return `curl ${getCurlInsecureFlag()}-fsSL ${url}/install.sh | bash -s -- --uninstall`;
const token = currentToken() || latestRecord()?.id || TOKEN_PLACEHOLDER;
const insecure = insecureMode() ? ' --insecure' : '';
return `curl ${getCurlInsecureFlag()}-fsSL ${url}/install.sh | bash -s -- --uninstall --url ${url} --token ${token}${insecure}`;
};
// Track previously seen host types to prevent flapping when one source temporarily has no data
@@ -1023,8 +1025,7 @@ export const UnifiedAgents: Component = () => {
<div class="flex items-center justify-end gap-3">
<button
onClick={async () => {
const url = agentUrl();
const cmd = `curl ${insecureMode() ? '-k' : ''}-fsSL ${url}/install.sh | bash -s -- --uninstall`;
const cmd = getUninstallCommand();
const success = await copyToClipboard(cmd);
if (success) {
notificationStore.success('Uninstall command copied');
+45
View File
@@ -298,3 +298,48 @@ func (h *HostAgentHandlers) handlePatchConfig(w http.ResponseWriter, r *http.Req
}
}
// HandleUninstall allows an agent to unregister itself during uninstallation.
// Requires ScopeHostReport and a valid hostId in the request body.
func (h *HostAgentHandlers) HandleUninstall(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only POST is allowed", nil)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 16*1024)
defer r.Body.Close()
var req struct {
HostID string `json:"hostId"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErrorResponse(w, http.StatusBadRequest, "invalid_json", "Failed to decode request body", map[string]string{"error": err.Error()})
return
}
hostID := strings.TrimSpace(req.HostID)
if hostID == "" {
writeErrorResponse(w, http.StatusBadRequest, "missing_host_id", "Host ID is required", nil)
return
}
log.Info().Str("hostId", hostID).Msg("Received unregistration request from agent uninstaller")
// Remove the host from state
_, err := h.monitor.RemoveHostAgent(hostID)
if err != nil {
// If host not found, we still return success because the goal is reached
log.Warn().Err(err).Str("hostId", hostID).Msg("Host not found during unregistration request")
}
go h.wsHub.BroadcastState(h.monitor.GetState().ToFrontend())
if err := utils.WriteJSONResponse(w, map[string]any{
"success": true,
"hostId": hostID,
"message": "Host unregistered successfully",
}); err != nil {
log.Error().Err(err).Msg("Failed to serialize host unregistration response")
}
}
+1
View File
@@ -205,6 +205,7 @@ func (r *Router) setupRoutes() {
r.mux.HandleFunc("/api/agents/kubernetes/report", RequireAuth(r.config, RequireScope(config.ScopeKubernetesReport, r.kubernetesAgentHandlers.HandleReport)))
r.mux.HandleFunc("/api/agents/host/report", RequireAuth(r.config, RequireScope(config.ScopeHostReport, r.hostAgentHandlers.HandleReport)))
r.mux.HandleFunc("/api/agents/host/lookup", RequireAuth(r.config, RequireScope(config.ScopeHostReport, r.hostAgentHandlers.HandleLookup)))
r.mux.HandleFunc("/api/agents/host/uninstall", RequireAuth(r.config, RequireScope(config.ScopeHostReport, r.hostAgentHandlers.HandleUninstall)))
// Host agent management routes - config endpoint is accessible by agents (GET) and admins (PATCH)
r.mux.HandleFunc("/api/agents/host/", RequireAuth(r.config, func(w http.ResponseWriter, req *http.Request) {
// Route /api/agents/host/{id}/config to HandleConfig
+29
View File
@@ -156,6 +156,35 @@ function Get-FileChecksum {
if ($Uninstall) {
Write-Host "Uninstalling $AgentName..." -ForegroundColor Cyan
# Try to notify the Pulse server about uninstallation if we have connection details
if (-not [string]::IsNullOrWhiteSpace($Url) -and -not [string]::IsNullOrWhiteSpace($Token)) {
# Try to recover agent ID if not provided
$detectedAgentId = $AgentId
$stateFile = "$env:ProgramData\Pulse\agent-id"
if ([string]::IsNullOrWhiteSpace($detectedAgentId) -and (Test-Path $stateFile)) {
$detectedAgentId = Get-Content $stateFile -Raw
if ($detectedAgentId) { $detectedAgentId = $detectedAgentId.Trim() }
}
if (-not [string]::IsNullOrWhiteSpace($detectedAgentId)) {
Write-Host "Notifying Pulse server to unregister agent ID: $detectedAgentId..." -ForegroundColor Gray
try {
$body = @{ hostId = $detectedAgentId } | ConvertTo-Json
$headers = @{ "X-API-Token" = $Token }
Invoke-RestMethod -Uri "$Url/api/agents/host/uninstall" `
-Method Post `
-Body $body `
-ContentType "application/json" `
-Headers $headers `
-TimeoutSec 5 `
-ErrorAction SilentlyContinue | Out-Null
} catch {
# Ignore errors during uninstall
}
}
}
if (Get-Service $AgentName -ErrorAction SilentlyContinue) {
Stop-Service $AgentName -Force -ErrorAction SilentlyContinue
$scOutput = sc.exe delete $AgentName 2>&1
+22
View File
@@ -272,6 +272,28 @@ log_info " Proxmox: $ENABLE_PROXMOX"
if [[ "$UNINSTALL" == "true" ]]; then
log_info "Uninstalling ${AGENT_NAME} and cleaning up legacy agents..."
# Try to notify the Pulse server about uninstallation if we have connection details
# This ensures the host record is removed and any linked PVE nodes are updated immediately.
if [[ -n "$PULSE_URL" && -n "$PULSE_TOKEN" ]]; then
# Try to recover agent ID if not provided
if [[ -z "$AGENT_ID" ]]; then
if [[ -f /var/lib/pulse-agent/agent-id ]]; then
AGENT_ID=$(cat /var/lib/pulse-agent/agent-id)
elif [[ -f "$TRUENAS_STATE_DIR/agent-id" ]]; then
AGENT_ID=$(cat "$TRUENAS_STATE_DIR/agent-id")
fi
fi
if [[ -n "$AGENT_ID" ]]; then
log_info "Notifying Pulse server to unregister agent ID: ${AGENT_ID}..."
CURL_ARGS=(-fsSL --connect-timeout 5 -X POST -H "Content-Type: application/json" -H "X-API-Token: ${PULSE_TOKEN}")
if [[ "$INSECURE" == "true" ]]; then CURL_ARGS+=(-k); fi
# Send unregistration request (ignore errors as we are uninstalling anyway)
curl "${CURL_ARGS[@]}" -d "{\"hostId\": \"${AGENT_ID}\"}" "${PULSE_URL}/api/agents/host/uninstall" >/dev/null 2>&1 || true
fi
fi
# Kill any running agent processes first
pkill -f "pulse-agent" 2>/dev/null || true
pkill -f "pulse-host-agent" 2>/dev/null || true