diff --git a/scripts/src/gc-remote-host-docker.py b/scripts/src/gc-remote-host-docker.py index 327f1a29..eda3fcf2 100644 --- a/scripts/src/gc-remote-host-docker.py +++ b/scripts/src/gc-remote-host-docker.py @@ -1,6 +1,50 @@ from datetime import datetime, timezone +def parse_docker_human_size(raw): + raw = str(raw).split("(")[0].strip() + match = re.match(r"^([0-9.]+)\s*([KMGT]?B)$", raw, re.I) + if not match: + return None + multiplier = {"B": 1, "KB": 1000, "MB": 1000**2, "GB": 1000**3, "TB": 1000**4}.get(match.group(2).upper(), 1) + return int(float(match.group(1)) * multiplier) + + +def parse_docker_build_cache(text): + for line in text.splitlines(): + if not line.startswith("Build Cache"): + continue + match = re.match(r"^Build Cache\s+\S+\s+\S+\s+(\S+)\s+(\S+)", line.strip()) + if not match: + continue + size = parse_docker_human_size(match.group(1)) + reclaim = parse_docker_human_size(match.group(2)) + if size is None or reclaim is None: + return None + return {"sizeBytes": size, "reclaimableBytes": reclaim} + return None + + +def build_cache_only_payload(observed_at): + disk_before = df_snapshot() + section = host_docker_gc_section("buildCachePrune") + until = str(OPTIONS.get("buildCacheUntil") or section.get("until") or "24h") + if not host_docker_gc_enabled("buildCachePrune"): + return {"ok": False, "action": "gc remote %s" % ACTION, "providerId": PROVIDER_ID, "scope": "build-cache-only", "dryRun": ACTION == "plan", "mutation": False, "diagnosis": host_docker_gc_reason("buildCachePrune", "BuildKit cache GC is disabled by YAML")} + system_df = command(["docker", "system", "df"], 8) + if system_df["exitCode"] != 0: + return {"ok": False, "action": "gc remote %s" % ACTION, "providerId": PROVIDER_ID, "scope": "build-cache-only", "dryRun": ACTION == "plan", "mutation": False, "diagnosis": bounded(system_df)} + cache = parse_docker_build_cache(system_df["stdout"]) + candidate = None + if cache is not None and cache["reclaimableBytes"] > 0: + candidate = {"id": "docker-builder:prune", "kind": "docker-build-cache-prune", "risk": "low", "sizeBytes": cache["sizeBytes"], "estimatedReclaimBytes": cache["reclaimableBytes"], "action": {"command": ["docker", "builder", "prune", "--all", "--force", "--filter", "until=%s" % until]}} + payload = {"ok": True, "action": "gc remote %s" % ACTION, "providerId": PROVIDER_ID, "scope": "build-cache-only", "configSource": "config/unidesk-cli.yaml#gc.remote.targets.%s.hostDockerGc.buildCachePrune" % PROVIDER_ID, "dryRun": ACTION == "plan", "mutation": ACTION == "run" and candidate is not None, "observedAt": observed_at, "diskBefore": disk_before, "until": until, "candidates": [candidate] if candidate else []} + if ACTION == "run" and candidate is not None: + payload["result"] = execute(candidate) + payload["diskAfter"] = df_snapshot() + return payload + + def docker_containers(): ps = command(["docker", "ps", "-qa", "--no-trunc"], 5) if ps["exitCode"] != 0 or not ps["stdout"].strip(): diff --git a/scripts/src/gc-remote-policy-runner.py b/scripts/src/gc-remote-policy-runner.py index 696bfa39..1f37cb0d 100644 --- a/scripts/src/gc-remote-policy-runner.py +++ b/scripts/src/gc-remote-policy-runner.py @@ -120,6 +120,10 @@ def run_json(args, timeout=45): } if process.returncode != 0: return None, result + try: + return json.loads(process.stdout or "{}"), result + except Exception: + return None, result def command_lines(args, timeout=30, limit=10000): @@ -150,10 +154,6 @@ def root_available_bytes(): return int(result["stdoutTail"].splitlines()[-1].strip()) except Exception: return None - try: - return json.loads(process.stdout or "{}"), result - except Exception: - return None, result def write_state(config, payload): diff --git a/scripts/src/gc-remote-runner.py b/scripts/src/gc-remote-runner.py index cfebd904..42a95251 100644 --- a/scripts/src/gc-remote-runner.py +++ b/scripts/src/gc-remote-runner.py @@ -1053,47 +1053,6 @@ def parse_journal_usage(text): mult = {"": 1, "K": 1024, "M": 1024**2, "G": 1024**3, "T": 1024**4}.get(m.group(2).upper(), 1) return int(float(m.group(1)) * mult) -def parse_docker_human_size(raw): - raw = str(raw).split("(")[0].strip() - m = re.match(r"^([0-9.]+)\s*([KMGT]?B)$", raw, re.I) - if not m: - return None - mult = {"B": 1, "KB": 1000, "MB": 1000**2, "GB": 1000**3, "TB": 1000**4}.get(m.group(2).upper(), 1) - return int(float(m.group(1)) * mult) - -def parse_docker_build_cache(text): - for line in text.splitlines(): - if not line.startswith("Build Cache"): - continue - match = re.match(r"^Build Cache\s+\S+\s+\S+\s+(\S+)\s+(\S+)", line.strip()) - if not match: - continue - size = parse_docker_human_size(match.group(1)) - reclaim = parse_docker_human_size(match.group(2)) - if size is None or reclaim is None: - return None - return {"sizeBytes": size, "reclaimableBytes": reclaim} - return None - -def build_cache_only_payload(observed_at): - disk_before = df_snapshot() - section = host_docker_gc_section("buildCachePrune") - until = str(OPTIONS.get("buildCacheUntil") or section.get("until") or "24h") - if not host_docker_gc_enabled("buildCachePrune"): - return {"ok": False, "action": "gc remote %s" % ACTION, "providerId": PROVIDER_ID, "scope": "build-cache-only", "dryRun": ACTION == "plan", "mutation": False, "diagnosis": host_docker_gc_reason("buildCachePrune", "BuildKit cache GC is disabled by YAML")} - system_df = command(["docker", "system", "df"], 8) - if system_df["exitCode"] != 0: - return {"ok": False, "action": "gc remote %s" % ACTION, "providerId": PROVIDER_ID, "scope": "build-cache-only", "dryRun": ACTION == "plan", "mutation": False, "diagnosis": bounded(system_df)} - cache = parse_docker_build_cache(system_df["stdout"]) - candidate = None - if cache is not None and cache["reclaimableBytes"] > 0: - candidate = {"id": "docker-builder:prune", "kind": "docker-build-cache-prune", "risk": "low", "sizeBytes": cache["sizeBytes"], "estimatedReclaimBytes": cache["reclaimableBytes"], "action": {"command": ["docker", "builder", "prune", "--all", "--force", "--filter", "until=%s" % until]}} - payload = {"ok": True, "action": "gc remote %s" % ACTION, "providerId": PROVIDER_ID, "scope": "build-cache-only", "configSource": "config/unidesk-cli.yaml#gc.remote.targets.%s.hostDockerGc.buildCachePrune" % PROVIDER_ID, "dryRun": ACTION == "plan", "mutation": ACTION == "run" and candidate is not None, "observedAt": observed_at, "diskBefore": disk_before, "until": until, "candidates": [candidate] if candidate else []} - if ACTION == "run" and candidate is not None: - payload["result"] = execute(candidate) - payload["diskAfter"] = df_snapshot() - return payload - # __UNIDESK_GC_REMOTE_HOST_DOCKER_HELPERS__ def cluster_preflight():