From 377ba0410c07cb83a9aaf7bcf7c8fd7697586e15 Mon Sep 17 00:00:00 2001 From: anti Date: Sat, 11 Apr 2026 03:46:52 -0400 Subject: [PATCH] feat(deploy): add --parallel flag for concurrent image builds When --parallel is set: - DOCKER_BUILDKIT=1 is injected into the subprocess environment to ensure BuildKit is active regardless of host daemon config - docker compose build runs first (all images built concurrently) - docker compose up -d follows without --build (no redundant checks) Without --parallel the original up --build path is preserved. --parallel and --no-cache compose correctly (build --no-cache). --- decnet/cli.py | 3 ++- decnet/deployer.py | 31 +++++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/decnet/cli.py b/decnet/cli.py index fc8d0f3..b1bd043 100644 --- a/decnet/cli.py +++ b/decnet/cli.py @@ -248,6 +248,7 @@ def deploy( mutate_interval: Optional[int] = typer.Option(30, "--mutate-interval", help="Automatically rotate services every N minutes"), dry_run: bool = typer.Option(False, "--dry-run", help="Generate compose file without starting containers"), no_cache: bool = typer.Option(False, "--no-cache", help="Force rebuild all images, ignoring Docker layer cache"), + parallel: bool = typer.Option(False, "--parallel", help="Build all images concurrently (enables BuildKit, separates build from up)"), ipvlan: bool = typer.Option(False, "--ipvlan", help="Use IPvlan L2 instead of MACVLAN (required on WiFi interfaces)"), config_file: Optional[str] = typer.Option(None, "--config", "-c", help="Path to INI config file"), api: bool = typer.Option(False, "--api", help="Start the FastAPI backend to ingest and serve logs"), @@ -379,7 +380,7 @@ def deploy( ) from decnet.deployer import deploy as _deploy - _deploy(config, dry_run=dry_run, no_cache=no_cache) + _deploy(config, dry_run=dry_run, no_cache=no_cache, parallel=parallel) if mutate_interval is not None and not dry_run: import subprocess # nosec B404 diff --git a/decnet/deployer.py b/decnet/deployer.py index c9b838b..92e2fb6 100644 --- a/decnet/deployer.py +++ b/decnet/deployer.py @@ -49,9 +49,11 @@ def _sync_logging_helper(config: DecnetConfig) -> None: shutil.copy2(_CANONICAL_LOGGING, dest) -def _compose(*args: str, compose_file: Path = COMPOSE_FILE) -> None: +def _compose(*args: str, compose_file: Path = COMPOSE_FILE, env: dict | None = None) -> None: + import os cmd = ["docker", "compose", "-f", str(compose_file), *args] - subprocess.run(cmd, check=True) # nosec B603 + merged = {**os.environ, **(env or {})} + subprocess.run(cmd, check=True, env=merged) # nosec B603 _PERMANENT_ERRORS = ( @@ -68,12 +70,15 @@ def _compose_with_retry( compose_file: Path = COMPOSE_FILE, retries: int = 3, delay: float = 5.0, + env: dict | None = None, ) -> None: """Run a docker compose command, retrying on transient failures.""" + import os last_exc: subprocess.CalledProcessError | None = None cmd = ["docker", "compose", "-f", str(compose_file), *args] + merged = {**os.environ, **(env or {})} for attempt in range(1, retries + 1): - result = subprocess.run(cmd, capture_output=True, text=True) # nosec B603 + result = subprocess.run(cmd, capture_output=True, text=True, env=merged) # nosec B603 if result.returncode == 0: if result.stdout: print(result.stdout, end="") @@ -100,7 +105,7 @@ def _compose_with_retry( raise last_exc -def deploy(config: DecnetConfig, dry_run: bool = False, no_cache: bool = False) -> None: +def deploy(config: DecnetConfig, dry_run: bool = False, no_cache: bool = False, parallel: bool = False) -> None: client = docker.from_env() # --- Network setup --- @@ -145,10 +150,24 @@ def deploy(config: DecnetConfig, dry_run: bool = False, no_cache: bool = False) save_state(config, compose_path) # --- Bring up --- + # With --parallel: force BuildKit, run build explicitly (so all images are + # built concurrently before any container starts), then up without --build. + # Without --parallel: keep the original up --build path. + build_env = {"DOCKER_BUILDKIT": "1"} if parallel else {} + console.print("[bold cyan]Building images and starting deckies...[/]") + build_args = ["build"] if no_cache: - _compose_with_retry("build", "--no-cache", compose_file=compose_path) - _compose_with_retry("up", "--build", "-d", compose_file=compose_path) + build_args.append("--no-cache") + + if parallel: + console.print("[bold cyan]Parallel build enabled — building all images concurrently...[/]") + _compose_with_retry(*build_args, compose_file=compose_path, env=build_env) + _compose_with_retry("up", "-d", compose_file=compose_path, env=build_env) + else: + if no_cache: + _compose_with_retry("build", "--no-cache", compose_file=compose_path) + _compose_with_retry("up", "--build", "-d", compose_file=compose_path) # --- Status summary --- _print_status(config)