From 47407cdbcab5204223f2486cb2a953e88d83b3f7 Mon Sep 17 00:00:00 2001 From: DungTT Date: Mon, 8 Jun 2026 10:54:52 +0700 Subject: [PATCH] remove app --- agent/app/config.py | 2 +- agent/app/core/docker_installer.py | 31 ++++++++++++- agent/app/core/installer.py | 7 +++ agent/app/core/service_manager.py | 4 +- agent/app/core/task_runner.py | 74 +++++++++++++++++++++++++++--- agent/packaging/DEBIAN/postinst | 1 + agent/scripts/build-deb.sh | 2 +- web-client/src/main.jsx | 2 +- web-client/src/services/api.js | 2 +- 9 files changed, 112 insertions(+), 13 deletions(-) diff --git a/agent/app/config.py b/agent/app/config.py index 9f8348f..4bdda7a 100644 --- a/agent/app/config.py +++ b/agent/app/config.py @@ -92,7 +92,7 @@ def get_settings() -> Settings: log_dir=Path(os.getenv("LOG_DIR", "/var/log/local-installer-agent")), db_path=Path(os.getenv("DB_PATH", "/var/lib/local-installer-agent/agent.db")), allow_remove=_bool("ALLOW_REMOVE", True), - allow_purge=_bool("ALLOW_PURGE", False), + allow_purge=_bool("ALLOW_PURGE", True), allow_docker=_bool("ALLOW_DOCKER", True), allow_docker_compose=_bool("ALLOW_DOCKER_COMPOSE", False), auto_install_docker=_bool("AUTO_INSTALL_DOCKER", True), diff --git a/agent/app/core/docker_installer.py b/agent/app/core/docker_installer.py index 4251f65..e774be8 100644 --- a/agent/app/core/docker_installer.py +++ b/agent/app/core/docker_installer.py @@ -140,7 +140,7 @@ class DockerInstaller: command.append(reference) self.command_runner.run(command) - def remove_container(self, container_name: str) -> None: + def remove_container(self, container_name: str, remove_volumes: bool = False) -> None: result = self.command_runner.run([ "docker", "ps", @@ -149,7 +149,34 @@ class DockerInstaller: f"name=^/{container_name}$", ]) if result.stdout.strip(): - self.command_runner.run(["docker", "rm", "-f", container_name]) + command = ["docker", "rm", "-f"] + if remove_volumes: + command.append("-v") + command.append(container_name) + self.command_runner.run(command) + + def remove_labeled_containers(self, app_id: str, component_id: str, remove_volumes: bool = False) -> None: + result = self.command_runner.run([ + "docker", + "ps", + "-aq", + "--filter", + f"label=local-installer-agent.app-id={app_id}", + "--filter", + f"label=local-installer-agent.component-id={component_id}", + ]) + container_ids = [line.strip() for line in result.stdout.splitlines() if line.strip()] + if not container_ids: + return + + command = ["docker", "rm", "-f"] + if remove_volumes: + command.append("-v") + command.extend(container_ids) + self.command_runner.run(command) + + def remove_image(self, reference: str) -> None: + self.command_runner.run(["docker", "image", "rm", reference]) def assert_container_running(self, container_name: str) -> None: result = self.command_runner.run([ diff --git a/agent/app/core/installer.py b/agent/app/core/installer.py index 260b0ea..190f7d2 100644 --- a/agent/app/core/installer.py +++ b/agent/app/core/installer.py @@ -83,6 +83,13 @@ class DebInstaller: env=APT_NONINTERACTIVE_ENV, ) + def cleanup_after_remove(self, purge: bool = False) -> None: + autoremove_command = ["apt-get", *APT_DPKG_OPTIONS, "autoremove", "--yes"] + if purge: + autoremove_command.append("--purge") + self.command_runner.run(autoremove_command, env=APT_NONINTERACTIVE_ENV) + self.command_runner.run(["apt-get", "clean"], env=APT_NONINTERACTIVE_ENV) + def get_package_version(self, package_name: str) -> str | None: result = self.command_runner.run(["dpkg-query", "-W", "-f=${Version}", package_name]) version = result.stdout.strip() diff --git a/agent/app/core/service_manager.py b/agent/app/core/service_manager.py index 5aea8ee..5a3d2ad 100644 --- a/agent/app/core/service_manager.py +++ b/agent/app/core/service_manager.py @@ -22,6 +22,9 @@ class ServiceManager: def restart_service(self, service_name: str) -> None: self.command_runner.run(["systemctl", "restart", service_name]) + def reset_failed(self, service_name: str) -> None: + self.command_runner.run(["systemctl", "reset-failed", service_name]) + def get_service_status(self, service_name: str) -> dict[str, object]: active = self._query(["systemctl", "is-active", service_name]) == "active" enabled = self._query(["systemctl", "is-enabled", service_name]) == "enabled" @@ -37,4 +40,3 @@ class ServiceManager: return self.command_runner.run(command).stdout.strip() except Exception: return "unknown" - diff --git a/agent/app/core/task_runner.py b/agent/app/core/task_runner.py index 0218e98..d7378a7 100644 --- a/agent/app/core/task_runner.py +++ b/agent/app/core/task_runner.py @@ -56,10 +56,15 @@ class TaskRunner: try: if not settings.allow_remove: raise ValueError("Remove is disabled on this Agent") - if request.purge and not settings.allow_purge: - raise ValueError("Purge is disabled on this Agent") self._mark_started(task_id, "starting remove") self._require_root_if_available() + effective_purge = request.purge and settings.allow_purge + if request.purge and not effective_purge: + self.repository.add_log( + task_id, + "warning", + "Purge cleanup was requested but ALLOW_PURGE is disabled; falling back to remove cleanup", + ) components = self.repository.list_installed_components(request.app_id) if not components and request.package_name: @@ -81,6 +86,7 @@ class TaskRunner: ordered = sorted(components, key=lambda item: item["install_order"], reverse=True) total = len(ordered) + removed_deb_package = False for index, component in enumerate(ordered, start=1): progress = int((index - 1) / total * 80) + 10 component_id = component["component_id"] @@ -93,22 +99,42 @@ class TaskRunner: service_name = component.get("service_name") if service_name: self.repository.add_log(task_id, "info", f"Stopping service {service_name}") - services.stop_service(service_name) - services.disable_service(service_name) + self._best_effort(task_id, f"stop service {service_name}", lambda: services.stop_service(service_name)) + self._best_effort(task_id, f"disable service {service_name}", lambda: services.disable_service(service_name)) package_name = component.get("package_name") if component["type"] == "deb" and package_name: self.repository.add_log(task_id, "info", f"Removing package {package_name}") - installer.remove_package(package_name, purge=request.purge) + installer.remove_package(package_name, purge=effective_purge) + self._clean_cached_package_files(task_id, package_name, component_id) + if service_name: + self._best_effort(task_id, f"reset failed state for {service_name}", lambda: services.reset_failed(service_name)) + removed_deb_package = True elif component["type"] == "docker": container_name = component.get("container_name") or component_id self.repository.add_log(task_id, "info", f"Removing Docker container {container_name}") docker_installer = DockerInstaller(command_runner) docker_installer.ensure_runtime(auto_install=settings.auto_install_docker) - docker_installer.remove_container(container_name) + docker_installer.remove_labeled_containers( + request.app_id, + component_id, + remove_volumes=effective_purge, + ) + docker_installer.remove_container(container_name, remove_volumes=effective_purge) + image = component.get("docker_image") or component.get("image") + if effective_purge and image: + self._best_effort(task_id, f"remove Docker image {image}", lambda: docker_installer.remove_image(image)) else: raise ValueError(f"Unsupported installed component type: {component['type']}") + if removed_deb_package: + self.repository.update_task(task_id, progress=92, current_step="cleaning package leftovers") + self._best_effort( + task_id, + "clean unused packages and apt cache", + lambda: installer.cleanup_after_remove(purge=effective_purge), + ) + self.repository.delete_installed_app(request.app_id) self.repository.update_task( task_id, @@ -323,3 +349,39 @@ class TaskRunner: geteuid = getattr(os, "geteuid", None) if callable(geteuid) and geteuid() != 0: raise PermissionError("Agent must run as root to call apt and systemctl") + + def _best_effort(self, task_id: str, action: str, callback: Any) -> None: + try: + callback() + except Exception as error: + self.repository.add_log(task_id, "warning", f"Could not {action}: {error}") + + def _clean_cached_package_files(self, task_id: str, *identifiers: str | None) -> None: + cache_dir = settings.cache_dir + if not cache_dir.exists() or not cache_dir.is_dir(): + return + + patterns: list[str] = [] + seen_patterns: set[str] = set() + for identifier in identifiers: + name = (identifier or "").strip() + if not name: + continue + for pattern in (f"{name}.deb", f"{name}_*.deb"): + if pattern not in seen_patterns: + patterns.append(pattern) + seen_patterns.add(pattern) + + removed_files: set[str] = set() + for pattern in patterns: + for file_path in cache_dir.glob(pattern): + if not file_path.is_file(): + continue + try: + file_path.unlink() + removed_files.add(str(file_path)) + except Exception as error: + self.repository.add_log(task_id, "warning", f"Could not remove cached package {file_path}: {error}") + + for file_path in sorted(removed_files): + self.repository.add_log(task_id, "info", f"Removed cached package {file_path}") diff --git a/agent/packaging/DEBIAN/postinst b/agent/packaging/DEBIAN/postinst index 5f0d956..3fec8a7 100644 --- a/agent/packaging/DEBIAN/postinst +++ b/agent/packaging/DEBIAN/postinst @@ -37,6 +37,7 @@ append_csv_env() { } set_agent_env ALLOW_DOCKER true +set_agent_env ALLOW_PURGE true set_agent_env AUTO_INSTALL_DOCKER true append_csv_env ALLOWED_DOCKER_REGISTRIES docker.io diff --git a/agent/scripts/build-deb.sh b/agent/scripts/build-deb.sh index bc6a612..e100519 100644 --- a/agent/scripts/build-deb.sh +++ b/agent/scripts/build-deb.sh @@ -66,7 +66,7 @@ APP_DIR=/opt/robot-apps LOG_DIR=/var/log/local-installer-agent DB_PATH=/var/lib/local-installer-agent/agent.db ALLOW_REMOVE=true -ALLOW_PURGE=false +ALLOW_PURGE=true ALLOW_DOCKER=true ALLOW_DOCKER_COMPOSE=false AUTO_INSTALL_DOCKER=true diff --git a/web-client/src/main.jsx b/web-client/src/main.jsx index 46c786f..800b26c 100644 --- a/web-client/src/main.jsx +++ b/web-client/src/main.jsx @@ -483,7 +483,7 @@ function App() { return; } - if (action === 'remove' && !window.confirm(`Remove ${app.appName} from ${agentTargetMachine}?`)) { + if (action === 'remove' && !window.confirm(`Remove and clean ${app.appName} from ${agentTargetMachine}?`)) { return; } diff --git a/web-client/src/services/api.js b/web-client/src/services/api.js index a8b7bad..f85ca63 100644 --- a/web-client/src/services/api.js +++ b/web-client/src/services/api.js @@ -216,7 +216,7 @@ export async function queueRemove(agentBaseUrl, app) { timeoutMs: 10000, body: { appId: app.appId, - purge: false + purge: true } }); }