mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
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:
@@ -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');
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user