From 91bee0832fd0fa5defd39984b82b05a8f76a92a9 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 5 Jul 2026 17:47:02 +0000 Subject: [PATCH] fix: protect jd01 host docker during remote gc --- config/unidesk-cli.yaml | 7 ++++++ scripts/src/gc-remote-runner.py | 39 +++++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/config/unidesk-cli.yaml b/config/unidesk-cli.yaml index 0bd980e4..171a05b6 100644 --- a/config/unidesk-cli.yaml +++ b/config/unidesk-cli.yaml @@ -109,6 +109,13 @@ gc: remote: targets: JD01: + hostDockerGc: + dockerJsonLogs: + enabled: false + reason: JD01 host Docker stores the provider-gateway/trans runtime; do not truncate provider container logs from generic remote GC. + buildCachePrune: + enabled: false + reason: JD01 host Docker stores the provider-gateway/trans runtime; Docker builder prune previously preceded snapshot/lowerdir corruption. memoryPressure: processPatterns: - chrome diff --git a/scripts/src/gc-remote-runner.py b/scripts/src/gc-remote-runner.py index 60ab0be8..511d29e0 100644 --- a/scripts/src/gc-remote-runner.py +++ b/scripts/src/gc-remote-runner.py @@ -22,6 +22,7 @@ CONTAINERD_CONFIG = REMOTE_TARGET.get("containerdImageCache") if isinstance(REMO HOST_CONTAINERD_CONFIG = REMOTE_TARGET.get("hostContainerdCache") if isinstance(REMOTE_TARGET.get("hostContainerdCache"), dict) else {} LOCAL_PATH_CONFIG = REMOTE_TARGET.get("localPathStorage") if isinstance(REMOTE_TARGET.get("localPathStorage"), dict) else {} POLICY_TIMER_CONFIG = REMOTE_TARGET.get("policyTimer") if isinstance(REMOTE_TARGET.get("policyTimer"), dict) else {} +HOST_DOCKER_GC_CONFIG = REMOTE_TARGET.get("hostDockerGc") if isinstance(REMOTE_TARGET.get("hostDockerGc"), dict) else {} POLICY_RUNNER_SOURCE = base64.b64decode("__UNIDESK_GC_REMOTE_POLICY_RUNNER_BASE64__").decode("utf-8") TMP_PREFIX_ALLOWLIST = [ @@ -142,6 +143,22 @@ def config_str(cfg, key, default=""): return value return str(default) +def host_docker_gc_section(name): + value = HOST_DOCKER_GC_CONFIG.get(name) if isinstance(HOST_DOCKER_GC_CONFIG, dict) else None + return value if isinstance(value, dict) else {} + +def host_docker_gc_enabled(name, default_enabled=True): + section = host_docker_gc_section(name) + if "enabled" in section: + return bool(section.get("enabled")) + if PROVIDER_ID.upper() == "JD01": + return False + return bool(default_enabled) + +def host_docker_gc_reason(name, fallback): + reason = host_docker_gc_section(name).get("reason") + return str(reason) if isinstance(reason, str) and reason else fallback + def parse_size_value(value, default=None): if isinstance(value, (int, float)) and value > 0: return int(value) @@ -374,11 +391,13 @@ def path_size(path): except OSError: return 0 -def du_size(path, timeout=20): +def du_size(path, timeout=20, fallback_walk=True): if not os.path.exists(path): return None result = command(["du", "-sxB1", path], timeout) if result["exitCode"] != 0: + if not fallback_walk: + return None return path_size(path) text = result["stdout"].strip() if not text: @@ -386,6 +405,8 @@ def du_size(path, timeout=20): try: return int(text.split()[0]) except Exception: + if not fallback_walk: + return None return path_size(path) def safe_int(value, default=0): @@ -931,11 +952,17 @@ def collect_protected(): ("docker-images-and-volumes", "docker-images-volumes", "Remote gc does not remove Docker images, containers, volumes or Compose projects."), ("k8s-api-objects", "deployments-statefulsets-secrets-pvcs", "Remote gc does not mutate Kubernetes workloads, Secrets, PVCs, PVs, Argo CD or Tekton objects."), ] + if not host_docker_gc_enabled("dockerJsonLogs"): + protected_paths.append(("host-docker-json-logs", "/var/lib/docker/containers", host_docker_gc_reason("dockerJsonLogs", "Host Docker json logs are protected on this provider."))) + if not host_docker_gc_enabled("buildCachePrune"): + protected_paths.append(("host-docker-build-cache", "docker-builder-cache", host_docker_gc_reason("buildCachePrune", "Host Docker builder cache prune is disabled on this provider."))) result = [] for kind, ref, reason in protected_paths: item = {"kind": kind, "risk": "blocked", "ref": ref, "reason": reason} if ref.startswith("/") and os.path.exists(ref): - item["sizeBytes"] = du_size(ref) + size = du_size(ref, 3, False) + item["sizeBytes"] = size + item["sizeState"] = "sampled" if size is not None else "timeout-or-unavailable" result.append(item) return result @@ -956,7 +983,7 @@ def collect_candidates(observed_at): "action": {"command": ["journalctl", "--vacuum-size=%s" % target]}, }) - if OPTIONS.get("dockerLogs", True): + if OPTIONS.get("dockerLogs", True) and host_docker_gc_enabled("dockerJsonLogs"): max_bytes = int(OPTIONS.get("dockerLogMaxBytes") or 52428800) for container in docker_containers(): path = container.get("logPath") or "" @@ -980,7 +1007,7 @@ def collect_candidates(observed_at): "action": {"op": "truncate", "targetBytes": 0}, }) - if OPTIONS.get("buildCache", True): + if OPTIONS.get("buildCache", True) and host_docker_gc_enabled("buildCachePrune"): system_df = command(["docker", "system", "df"], 8) if system_df["exitCode"] == 0: cache = parse_docker_build_cache(system_df["stdout"]) @@ -1334,6 +1361,8 @@ def execute(candidate): raise RuntimeError((result["stderr"] or "journalctl vacuum failed").strip()) return {"reclaimedBytes": None, "commandOutput": bounded(result)} if kind == "docker-json-log-truncate": + if not host_docker_gc_enabled("dockerJsonLogs"): + raise RuntimeError(host_docker_gc_reason("dockerJsonLogs", "refusing Docker json log truncation on this provider")) path = candidate.get("path") or "" if not path.startswith("/var/lib/docker/containers/"): raise RuntimeError("refusing to truncate Docker log outside /var/lib/docker/containers") @@ -1342,6 +1371,8 @@ def execute(candidate): handle.truncate(0) return {"reclaimedBytes": before} if kind == "docker-build-cache-prune": + if not host_docker_gc_enabled("buildCachePrune"): + raise RuntimeError(host_docker_gc_reason("buildCachePrune", "refusing Docker builder prune on this provider")) until = str(OPTIONS.get("buildCacheUntil") or "24h") result = command(["docker", "builder", "prune", "--all", "--force", "--filter", "until=%s" % until], 45) if result["exitCode"] != 0: