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")),
|
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")),
|
db_path=Path(os.getenv("DB_PATH", "/var/lib/local-installer-agent/agent.db")),
|
||||||
allow_remove=_bool("ALLOW_REMOVE", True),
|
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=_bool("ALLOW_DOCKER", True),
|
||||||
allow_docker_compose=_bool("ALLOW_DOCKER_COMPOSE", False),
|
allow_docker_compose=_bool("ALLOW_DOCKER_COMPOSE", False),
|
||||||
auto_install_docker=_bool("AUTO_INSTALL_DOCKER", True),
|
auto_install_docker=_bool("AUTO_INSTALL_DOCKER", True),
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ class DockerInstaller:
|
|||||||
command.append(reference)
|
command.append(reference)
|
||||||
self.command_runner.run(command)
|
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([
|
result = self.command_runner.run([
|
||||||
"docker",
|
"docker",
|
||||||
"ps",
|
"ps",
|
||||||
@@ -149,7 +149,34 @@ class DockerInstaller:
|
|||||||
f"name=^/{container_name}$",
|
f"name=^/{container_name}$",
|
||||||
])
|
])
|
||||||
if result.stdout.strip():
|
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:
|
def assert_container_running(self, container_name: str) -> None:
|
||||||
result = self.command_runner.run([
|
result = self.command_runner.run([
|
||||||
|
|||||||
@@ -83,6 +83,13 @@ class DebInstaller:
|
|||||||
env=APT_NONINTERACTIVE_ENV,
|
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:
|
def get_package_version(self, package_name: str) -> str | None:
|
||||||
result = self.command_runner.run(["dpkg-query", "-W", "-f=${Version}", package_name])
|
result = self.command_runner.run(["dpkg-query", "-W", "-f=${Version}", package_name])
|
||||||
version = result.stdout.strip()
|
version = result.stdout.strip()
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ class ServiceManager:
|
|||||||
def restart_service(self, service_name: str) -> None:
|
def restart_service(self, service_name: str) -> None:
|
||||||
self.command_runner.run(["systemctl", "restart", service_name])
|
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]:
|
def get_service_status(self, service_name: str) -> dict[str, object]:
|
||||||
active = self._query(["systemctl", "is-active", service_name]) == "active"
|
active = self._query(["systemctl", "is-active", service_name]) == "active"
|
||||||
enabled = self._query(["systemctl", "is-enabled", service_name]) == "enabled"
|
enabled = self._query(["systemctl", "is-enabled", service_name]) == "enabled"
|
||||||
@@ -37,4 +40,3 @@ class ServiceManager:
|
|||||||
return self.command_runner.run(command).stdout.strip()
|
return self.command_runner.run(command).stdout.strip()
|
||||||
except Exception:
|
except Exception:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
|
|
||||||
|
|||||||
@@ -56,10 +56,15 @@ class TaskRunner:
|
|||||||
try:
|
try:
|
||||||
if not settings.allow_remove:
|
if not settings.allow_remove:
|
||||||
raise ValueError("Remove is disabled on this Agent")
|
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._mark_started(task_id, "starting remove")
|
||||||
self._require_root_if_available()
|
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)
|
components = self.repository.list_installed_components(request.app_id)
|
||||||
if not components and request.package_name:
|
if not components and request.package_name:
|
||||||
@@ -81,6 +86,7 @@ class TaskRunner:
|
|||||||
|
|
||||||
ordered = sorted(components, key=lambda item: item["install_order"], reverse=True)
|
ordered = sorted(components, key=lambda item: item["install_order"], reverse=True)
|
||||||
total = len(ordered)
|
total = len(ordered)
|
||||||
|
removed_deb_package = False
|
||||||
for index, component in enumerate(ordered, start=1):
|
for index, component in enumerate(ordered, start=1):
|
||||||
progress = int((index - 1) / total * 80) + 10
|
progress = int((index - 1) / total * 80) + 10
|
||||||
component_id = component["component_id"]
|
component_id = component["component_id"]
|
||||||
@@ -93,22 +99,42 @@ class TaskRunner:
|
|||||||
service_name = component.get("service_name")
|
service_name = component.get("service_name")
|
||||||
if service_name:
|
if service_name:
|
||||||
self.repository.add_log(task_id, "info", f"Stopping service {service_name}")
|
self.repository.add_log(task_id, "info", f"Stopping service {service_name}")
|
||||||
services.stop_service(service_name)
|
self._best_effort(task_id, f"stop service {service_name}", lambda: services.stop_service(service_name))
|
||||||
services.disable_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")
|
package_name = component.get("package_name")
|
||||||
if component["type"] == "deb" and package_name:
|
if component["type"] == "deb" and package_name:
|
||||||
self.repository.add_log(task_id, "info", f"Removing package {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":
|
elif component["type"] == "docker":
|
||||||
container_name = component.get("container_name") or component_id
|
container_name = component.get("container_name") or component_id
|
||||||
self.repository.add_log(task_id, "info", f"Removing Docker container {container_name}")
|
self.repository.add_log(task_id, "info", f"Removing Docker container {container_name}")
|
||||||
docker_installer = DockerInstaller(command_runner)
|
docker_installer = DockerInstaller(command_runner)
|
||||||
docker_installer.ensure_runtime(auto_install=settings.auto_install_docker)
|
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:
|
else:
|
||||||
raise ValueError(f"Unsupported installed component type: {component['type']}")
|
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.delete_installed_app(request.app_id)
|
||||||
self.repository.update_task(
|
self.repository.update_task(
|
||||||
task_id,
|
task_id,
|
||||||
@@ -323,3 +349,39 @@ class TaskRunner:
|
|||||||
geteuid = getattr(os, "geteuid", None)
|
geteuid = getattr(os, "geteuid", None)
|
||||||
if callable(geteuid) and geteuid() != 0:
|
if callable(geteuid) and geteuid() != 0:
|
||||||
raise PermissionError("Agent must run as root to call apt and systemctl")
|
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_DOCKER true
|
||||||
|
set_agent_env ALLOW_PURGE true
|
||||||
set_agent_env AUTO_INSTALL_DOCKER true
|
set_agent_env AUTO_INSTALL_DOCKER true
|
||||||
append_csv_env ALLOWED_DOCKER_REGISTRIES docker.io
|
append_csv_env ALLOWED_DOCKER_REGISTRIES docker.io
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ APP_DIR=/opt/robot-apps
|
|||||||
LOG_DIR=/var/log/local-installer-agent
|
LOG_DIR=/var/log/local-installer-agent
|
||||||
DB_PATH=/var/lib/local-installer-agent/agent.db
|
DB_PATH=/var/lib/local-installer-agent/agent.db
|
||||||
ALLOW_REMOVE=true
|
ALLOW_REMOVE=true
|
||||||
ALLOW_PURGE=false
|
ALLOW_PURGE=true
|
||||||
ALLOW_DOCKER=true
|
ALLOW_DOCKER=true
|
||||||
ALLOW_DOCKER_COMPOSE=false
|
ALLOW_DOCKER_COMPOSE=false
|
||||||
AUTO_INSTALL_DOCKER=true
|
AUTO_INSTALL_DOCKER=true
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ function App() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'remove' && !window.confirm(`Remove ${app.appName} from ${agentTargetMachine}?`)) {
|
if (action === 'remove' && !window.confirm(`Remove and clean ${app.appName} from ${agentTargetMachine}?`)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ export async function queueRemove(agentBaseUrl, app) {
|
|||||||
timeoutMs: 10000,
|
timeoutMs: 10000,
|
||||||
body: {
|
body: {
|
||||||
appId: app.appId,
|
appId: app.appId,
|
||||||
purge: false
|
purge: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user