remove app
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ export async function queueRemove(agentBaseUrl, app) {
|
||||
timeoutMs: 10000,
|
||||
body: {
|
||||
appId: app.appId,
|
||||
purge: false
|
||||
purge: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user