"""Select a reachable build route and manage only Planet-owned daemon proxy settings.""" from __future__ import annotations import concurrent.futures import fcntl import json import os from pathlib import Path import re import subprocess import sys import tempfile from urllib.parse import urlsplit CONFIG = Path("/etc/docker/daemon.json") STATE = Path("/etc/docker/planet-proxy-state.json") PROXY_KEYS = ("http-proxy", "https-proxy", "no-proxy") PROXY_ENV = ("HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy") PROXY_SCHEMES = ("http", "https") DEFAULT_NO_PROXY = "localhost,127.0.0.1,::1" CONFIG_ERROR = "PLANET_PROXY_CONFIG_FAILED" COMMAND_TIMEOUT_SECONDS = 60 CONNECT_TIMEOUT_SECONDS = 3 REQUEST_TIMEOUT_SECONDS = 8 PROBE_PROCESS_TIMEOUT_SECONDS = 10 class ProxyError(Exception): pass def run(args: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: return subprocess.run( args, text=True, capture_output=True, check=True, timeout=COMMAND_TIMEOUT_SECONDS, **kwargs ) def daemon_proxies() -> dict[str, str]: info = json.loads(run(["docker", "info", "--format", "{{json .}}"]).stdout) values = dict( zip(PROXY_KEYS, (info.get("HttpProxy"), info.get("HttpsProxy"), info.get("NoProxy"))) ) return {key: value for key, value in values.items() if value} def registry_urls(environment: dict[str, str]) -> list[str]: dockerfile = Path(__file__).resolve().parents[1] / "aiprovider/Dockerfile" defaults = dict(re.findall(r"^ARG (PYTHON_IMAGE|UV_IMAGE)=(.+)$", dockerfile.read_text(), re.M)) registries = set() for key in ("PYTHON_IMAGE", "UV_IMAGE"): image = environment.get(key) or defaults[key] prefix = image.split("/")[0] registry = ( prefix if "/" in image and ("." in prefix or ":" in prefix or prefix == "localhost") else "docker.io" ) registries.add("registry-1.docker.io" if registry == "docker.io" else registry) return [f"https://{registry}/v2/" for registry in sorted(registries)] def probe_url(url: str, proxy: str, no_proxy: str = "") -> bool: environment = { key: value for key, value in os.environ.items() if key.lower() not in ("http_proxy", "https_proxy", "all_proxy", "no_proxy") } if proxy: environment.update(http_proxy=proxy, https_proxy=proxy) try: result = subprocess.run( [ "curl", "-q", "--silent", "--output", "/dev/null", "--write-out", "%{http_code}", "--noproxy", no_proxy if proxy else "", "--connect-timeout", str(CONNECT_TIMEOUT_SECONDS), "--max-time", str(REQUEST_TIMEOUT_SECONDS), url, ], env=environment, text=True, capture_output=True, timeout=PROBE_PROCESS_TIMEOUT_SECONDS, ) return result.returncode == 0 and ( result.stdout.startswith("2") or result.stdout in ("401", "403", "429") ) except (OSError, subprocess.TimeoutExpired): return False def reachable(urls: list[str], proxy: str, no_proxy: str = "") -> bool: with concurrent.futures.ThreadPoolExecutor(max_workers=len(urls)) as pool: return all(pool.map(lambda url: probe_url(url, proxy, no_proxy), urls)) def select_route(environment: dict[str, str], urls: list[str]) -> dict[str, object]: candidates = list(dict.fromkeys(environment[key] for key in PROXY_ENV if environment.get(key))) no_proxy = environment.get("NO_PROXY") or environment.get("no_proxy") or DEFAULT_NO_PROXY for proxy in candidates: if urlsplit(proxy).scheme not in PROXY_SCHEMES: continue if reachable(urls, proxy, no_proxy): return { "desired": dict(zip(PROXY_KEYS, (proxy, proxy, no_proxy))), "mode": "proxy", "reachable": True, } return {"desired": {}, "mode": "direct", "reachable": reachable(urls, "")} def atomic_write(path: Path, contents: bytes) -> None: path.parent.mkdir(parents=True, exist_ok=True) descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) try: with os.fdopen(descriptor, "wb") as output: output.write(contents) os.replace(temporary, path) finally: Path(temporary).unlink(missing_ok=True) def encode(value: dict[str, object]) -> bytes: return (json.dumps(value, indent=2) + "\n").encode() def validate_plan(plan: dict[str, object]) -> None: for field in ("current", "desired"): values = plan.get(field) if not isinstance(values, dict) or set(values) - set(PROXY_KEYS): raise ProxyError(CONFIG_ERROR) if any(not isinstance(value, str) or "\n" in value for value in values.values()): raise ProxyError(CONFIG_ERROR) for key in ("http-proxy", "https-proxy"): value = plan["desired"].get(key, "") if value and urlsplit(value).scheme not in PROXY_SCHEMES: raise ProxyError(CONFIG_ERROR) def restore_containers(containers: list[str]) -> None: if containers: run(["docker", "start", *containers]) def apply_plan(plan: dict[str, object], config: Path = CONFIG, state: Path = STATE) -> None: validate_plan(plan) current = daemon_proxies() if current != plan["current"]: raise ProxyError("PLANET_PROXY_CONFIG_CHANGED") if current == plan["desired"]: return original = config.read_bytes() if config.exists() else None data = json.loads(original) if original else {} owned = json.loads(state.read_text()) if state.exists() else None configured = {key: value for key, value in data.get("proxies", {}).items() if value} if (configured or current) and ( not owned or owned.get("managed") != configured or current != configured ): raise ProxyError("PLANET_PROXY_EXTERNAL") if plan["desired"]: data["proxies"] = plan["desired"] else: data.pop("proxies", None) containers = run(["docker", "ps", "-q"]).stdout.split() if original is not None: atomic_write(config.with_suffix(".json.planet-proxy.bak"), original) atomic_write(config, encode(data)) restart_requested = False try: run(["dockerd", "--validate", "--config-file", str(config)]) restart_requested = True run(["systemctl", "restart", "docker"]) restore_containers(containers) if daemon_proxies() != plan["desired"]: raise ProxyError(CONFIG_ERROR) atomic_write(state, encode({"managed": plan["desired"]})) except Exception as error: rollback(config, original, containers, restart_requested) raise ProxyError(CONFIG_ERROR) from error def rollback( config: Path, original: bytes | None, containers: list[str], restart_requested: bool ) -> None: if original is None: config.unlink(missing_ok=True) else: atomic_write(config, original) if not restart_requested: return try: run(["systemctl", "restart", "docker"]) restore_containers(containers) except Exception as error: raise ProxyError("PLANET_PROXY_ROLLBACK_FAILED") from error def main() -> None: action = sys.argv[1] if action == "plan": plan = select_route(dict(os.environ), registry_urls(dict(os.environ))) plan["current"] = daemon_proxies() json.dump(plan, sys.stdout) elif action == "status": plan = json.load(sys.stdin) changed = int(plan["current"] != plan["desired"]) print(plan["mode"], changed, int(plan["reachable"])) elif action == "apply": if os.geteuid() != 0: raise ProxyError(CONFIG_ERROR) CONFIG.parent.mkdir(parents=True, exist_ok=True) with (CONFIG.parent / "planet-proxy.lock").open("a") as lock: fcntl.flock(lock, fcntl.LOCK_EX) apply_plan(json.load(sys.stdin)) else: raise ProxyError(CONFIG_ERROR) if __name__ == "__main__": try: main() except ProxyError as error: sys.stderr.write(str(error) + "\n") sys.exit(1) except Exception: # Subprocess output and proxy URLs may contain credentials. sys.stderr.write(CONFIG_ERROR + "\n") sys.exit(1)