From 012aa13f3e1a11a342e39a168e9ef82babe6f9e1 Mon Sep 17 00:00:00 2001 From: Deeman Date: Fri, 27 Feb 2026 17:18:50 +0100 Subject: [PATCH] fix: replace abandoned docker-autocompose with inline Python script docker-autocompose 1.0.1 uses distutils (removed in Python 3.12) and the old docker SDK (docker.Client). Replace with a small inline script using docker>=7.0 + pyyaml, run via uv --with inline dependencies. Uses com.docker.compose.service label for correct service names. Co-Authored-By: Claude Sonnet 4.6 --- bootstrap.sh | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/bootstrap.sh b/bootstrap.sh index 871e4e5..c659705 100644 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -45,6 +45,92 @@ command -v docker >/dev/null 2>&1 || { echo "ERROR: docker not found"; exit 1; } [ -f "${UV}" ] || { echo "ERROR: uv not found at ${UV}. Run setup.sh first."; exit 1; } +# ── Inline autocompose script (replaces abandoned docker-autocompose package) ── +# Uses modern docker SDK. Written to /tmp, run via uv (fetches deps on demand). + +cat > /tmp/server-infra-autocompose.py << 'PYEOF' +# /// script +# requires-python = ">=3.10" +# dependencies = ["docker>=7.0", "pyyaml>=6.0"] +# /// +"""Generate a docker-compose services block from running containers.""" +import sys +import docker +import yaml + + +def container_to_service(container): + attrs = container.attrs + config = attrs["Config"] + host_config = attrs["HostConfig"] + + service = {"image": config["Image"]} + + restart = (host_config.get("RestartPolicy") or {}).get("Name", "no") + if restart and restart not in ("no", ""): + service["restart"] = restart + + env = config.get("Env") or [] + if env: + service["environment"] = env + + port_bindings = host_config.get("PortBindings") or {} + if port_bindings: + ports = [] + for container_port, bindings in port_bindings.items(): + for binding in (bindings or []): + host_ip = binding.get("HostIp", "") + host_port = binding.get("HostPort", "") + if host_ip and host_ip not in ("0.0.0.0", "::"): + ports.append(f"{host_ip}:{host_port}:{container_port}") + elif host_port: + ports.append(f"{host_port}:{container_port}") + else: + ports.append(container_port) + if ports: + service["ports"] = ports + + volumes = [] + for mount in attrs.get("Mounts") or []: + dst = mount["Destination"] + if mount["Type"] == "bind": + src = mount["Source"] + mode = mount.get("Mode", "rw") + volumes.append(f"{src}:{dst}" if mode == "rw" else f"{src}:{dst}:{mode}") + elif mount["Type"] == "volume": + name = mount.get("Name", "") + if name: + volumes.append(f"{name}:{dst}") + if volumes: + service["volumes"] = volumes + + return service + + +def main(): + if len(sys.argv) < 2: + print("Usage: autocompose.py [...]", file=sys.stderr) + sys.exit(1) + + client = docker.from_env() + services = {} + + for name in sys.argv[1:]: + try: + container = client.containers.get(name) + labels = (container.attrs["Config"].get("Labels") or {}) + service_name = labels.get("com.docker.compose.service", name) + services[service_name] = container_to_service(container) + except docker.errors.NotFound: + print(f"WARNING: Container {name} not found", file=sys.stderr) + + print(yaml.dump({"services": services}, default_flow_style=False, sort_keys=False)) + + +if __name__ == "__main__": + main() +PYEOF + # ── Recover compose files from running containers ────────────────────────────── recover_project() { @@ -67,7 +153,7 @@ recover_project() { log "Recovering: ${containers[*]}" sudo -u "${SERVICE_USER}" \ - "/home/${SERVICE_USER}/.local/bin/uvx" --from docker-autocompose autocompose "${containers[@]}" > "${outfile}" + "${UV}" run /tmp/server-infra-autocompose.py "${containers[@]}" > "${outfile}" log " Saved to ${outfile}" }