From 0c1d68eb01494e73206fdd7a0c965fb0bf181cc6 Mon Sep 17 00:00:00 2001 From: taylanbakircioglu Date: Mon, 26 Jan 2026 15:25:15 +0300 Subject: [PATCH] feat: add uninstall script UI with modern design - Add new API endpoint to serve uninstall scripts by platform - Display uninstall script alongside install script in setup wizard - Add dedicated delete agent modal with 2-step workflow - Modern UI with gradient banners, platform icons, and info cards - Enhanced uninstall scripts to clean all agent temp/backup files - HAProxy service and config remain untouched during uninstall --- backend/routers/agent.py | 86 +++ frontend/src/components/AgentManagement.js | 631 ++++++++++++++++++--- utils/uninstall-agent-linux.sh | 30 + utils/uninstall-agent-macos.sh | 32 ++ 4 files changed, 705 insertions(+), 74 deletions(-) diff --git a/backend/routers/agent.py b/backend/routers/agent.py index c5fc3bb..a2206c5 100644 --- a/backend/routers/agent.py +++ b/backend/routers/agent.py @@ -650,6 +650,92 @@ async def generate_install_script(req_data: AgentScriptRequest, request: Request logger.error(f"Failed to generate install script: {e}") raise HTTPException(status_code=500, detail=f"Failed to generate install script: {str(e)}") +@router.get("/generate-uninstall-script/{platform}", summary="Generate Agent Uninstall Script", response_description="Uninstall script") +async def generate_uninstall_script(platform: str, authorization: str = Header(None), x_api_key: Optional[str] = Header(None)): + """ + # Generate Agent Uninstall Script + + Generate platform-specific uninstall script for completely removing agent from HAProxy servers. + This script removes ONLY agent-related files and services - HAProxy itself remains untouched. + + ## Path Parameters + - **platform**: Platform type: "macos" or "linux" (required) + + ## What Gets Removed + - Agent service (systemd/launchd) + - Agent binary (/usr/local/bin/haproxy-agent) + - Agent configuration (/etc/haproxy-agent) + - Agent logs (/var/log/haproxy-agent) + - Agent temp files and upgrade markers + - Agent backup files + + ## What Is NOT Touched + - HAProxy service + - HAProxy configuration (/etc/haproxy/haproxy.cfg) + - HAProxy logs + - Any other HAProxy-related files + + ## Example Request + ```bash + curl -X GET "{BASE_URL}/api/agents/generate-uninstall-script/linux" \\ + -H "Authorization: Bearer eyJhbGciOiJIUz..." + ``` + + ## Usage on Remote Server + ```bash + # Save the script + curl -o uninstall-agent.sh "{BASE_URL}/api/agents/generate-uninstall-script/linux" + chmod +x uninstall-agent.sh + sudo ./uninstall-agent.sh + ``` + """ + try: + # Validate platform + platform_lower = platform.lower() + if platform_lower not in ['linux', 'macos']: + raise HTTPException( + status_code=400, + detail=f"Invalid platform: {platform}. Must be 'linux' or 'macos'" + ) + + # Read uninstall script from utils directory + import os + script_filename = f"uninstall-agent-{platform_lower}.sh" + + # Try multiple possible paths + possible_paths = [ + os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "utils", script_filename), + os.path.join(os.path.dirname(os.path.dirname(__file__)), "utils", script_filename), + f"/app/utils/{script_filename}", + ] + + script_content = None + for path in possible_paths: + if os.path.exists(path): + with open(path, 'r') as f: + script_content = f.read() + break + + if not script_content: + logger.error(f"Uninstall script not found for platform: {platform_lower}") + raise HTTPException( + status_code=404, + detail=f"Uninstall script not found for platform: {platform_lower}" + ) + + return { + "platform": platform_lower, + "script": script_content, + "filename": script_filename, + "usage": f"chmod +x {script_filename} && sudo ./{script_filename}" + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to generate uninstall script: {e}") + raise HTTPException(status_code=500, detail=f"Failed to generate uninstall script: {str(e)}") + @router.delete("/{agent_id}", summary="Delete Agent", response_description="Agent deleted successfully") async def delete_agent(agent_id: int, authorization: str = Header(None)): """ diff --git a/frontend/src/components/AgentManagement.js b/frontend/src/components/AgentManagement.js index d5063dc..5ef72b3 100644 --- a/frontend/src/components/AgentManagement.js +++ b/frontend/src/components/AgentManagement.js @@ -157,8 +157,16 @@ const AgentManagement = () => { const [poolClusters, setPoolClusters] = useState([]); const [selectedClusterId, setSelectedClusterId] = useState(null); const [installScript, setInstallScript] = useState(''); + const [uninstallScript, setUninstallScript] = useState(''); const [scriptGenerating, setScriptGenerating] = useState(false); + // Delete Agent Modal state + const [deleteModalVisible, setDeleteModalVisible] = useState(false); + const [agentToDelete, setAgentToDelete] = useState(null); + const [deleteUninstallScript, setDeleteUninstallScript] = useState(''); + const [deleteConfirmText, setDeleteConfirmText] = useState(''); + const [deleting, setDeleting] = useState(false); + // Pools state for the setup wizard const [supportedPlatforms, setSupportedPlatforms] = useState({}); @@ -457,6 +465,20 @@ const AgentManagement = () => { }); setInstallScript(response.data.script); + + // Also fetch uninstall script for this platform + try { + const uninstallResponse = await axios.get(`/api/agents/generate-uninstall-script/${selectedPlatform}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('authToken') || ''}`, + }, + }); + setUninstallScript(uninstallResponse.data.script); + } catch (uninstallError) { + console.warn('Failed to fetch uninstall script:', uninstallError); + setUninstallScript(''); // Clear on error + } + setCurrentStep(3); // Move to script step notification.success({ @@ -488,6 +510,7 @@ const AgentManagement = () => { setPoolClusters([]); setSelectedClusterId(null); setInstallScript(''); + setUninstallScript(''); }; // Generate random hostname @@ -529,9 +552,42 @@ const AgentManagement = () => { } }; + // Open delete agent modal with uninstall script + const openDeleteModal = async (agent) => { + setAgentToDelete(agent); + setDeleteConfirmText(''); + setDeleteModalVisible(true); + + // Fetch uninstall script for agent's platform + const platform = agent.platform || 'linux'; + try { + const response = await axios.get(`/api/agents/generate-uninstall-script/${platform}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('authToken') || ''}`, + }, + }); + setDeleteUninstallScript(response.data.script); + } catch (error) { + console.warn('Failed to fetch uninstall script:', error); + setDeleteUninstallScript(''); + } + }; + + // Close delete modal + const closeDeleteModal = () => { + setDeleteModalVisible(false); + setAgentToDelete(null); + setDeleteUninstallScript(''); + setDeleteConfirmText(''); + }; + // Enhanced delete agent - const deleteAgent = async (agentId, agentName) => { - const hide = message.loading(`Deleting agent ${agentName}...`); + const deleteAgent = async () => { + if (!agentToDelete) return; + + const { id: agentId, name: agentName } = agentToDelete; + setDeleting(true); + try { // Optimistic update: Remove agent from local state immediately const updatedAgents = agents.filter(agent => agent.id !== agentId); @@ -547,10 +603,12 @@ const AgentManagement = () => { notification.success({ message: 'Agent Deleted', - description: `Agent "${agentName}" has been successfully removed.`, + description: `Agent "${agentName}" has been successfully removed from the system.`, duration: 4 }); + closeDeleteModal(); + // Refresh from server to ensure consistency (force=true to bypass throttling) await fetchAgents(true); } catch (error) { @@ -564,7 +622,7 @@ const AgentManagement = () => { // Revert optimistic update on error (force=true to bypass throttling) await fetchAgents(true); } finally { - hide(); + setDeleting(false); } }; @@ -1346,24 +1404,15 @@ const AgentManagement = () => { onClick={() => showAgentDetails(record)} /> - deleteAgent(record.id, record.name)} - okText="Delete" - cancelText="Cancel" - okButtonProps={{ danger: true }} - icon={} - > - - + + +