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).
This commit is contained in:
@@ -248,6 +248,7 @@ def deploy(
|
|||||||
mutate_interval: Optional[int] = typer.Option(30, "--mutate-interval", help="Automatically rotate services every N minutes"),
|
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"),
|
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"),
|
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)"),
|
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"),
|
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"),
|
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
|
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:
|
if mutate_interval is not None and not dry_run:
|
||||||
import subprocess # nosec B404
|
import subprocess # nosec B404
|
||||||
|
|||||||
@@ -49,9 +49,11 @@ def _sync_logging_helper(config: DecnetConfig) -> None:
|
|||||||
shutil.copy2(_CANONICAL_LOGGING, dest)
|
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]
|
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 = (
|
_PERMANENT_ERRORS = (
|
||||||
@@ -68,12 +70,15 @@ def _compose_with_retry(
|
|||||||
compose_file: Path = COMPOSE_FILE,
|
compose_file: Path = COMPOSE_FILE,
|
||||||
retries: int = 3,
|
retries: int = 3,
|
||||||
delay: float = 5.0,
|
delay: float = 5.0,
|
||||||
|
env: dict | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Run a docker compose command, retrying on transient failures."""
|
"""Run a docker compose command, retrying on transient failures."""
|
||||||
|
import os
|
||||||
last_exc: subprocess.CalledProcessError | None = None
|
last_exc: subprocess.CalledProcessError | None = None
|
||||||
cmd = ["docker", "compose", "-f", str(compose_file), *args]
|
cmd = ["docker", "compose", "-f", str(compose_file), *args]
|
||||||
|
merged = {**os.environ, **(env or {})}
|
||||||
for attempt in range(1, retries + 1):
|
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.returncode == 0:
|
||||||
if result.stdout:
|
if result.stdout:
|
||||||
print(result.stdout, end="")
|
print(result.stdout, end="")
|
||||||
@@ -100,7 +105,7 @@ def _compose_with_retry(
|
|||||||
raise last_exc
|
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()
|
client = docker.from_env()
|
||||||
|
|
||||||
# --- Network setup ---
|
# --- Network setup ---
|
||||||
@@ -145,7 +150,21 @@ def deploy(config: DecnetConfig, dry_run: bool = False, no_cache: bool = False)
|
|||||||
save_state(config, compose_path)
|
save_state(config, compose_path)
|
||||||
|
|
||||||
# --- Bring up ---
|
# --- 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...[/]")
|
console.print("[bold cyan]Building images and starting deckies...[/]")
|
||||||
|
build_args = ["build"]
|
||||||
|
if no_cache:
|
||||||
|
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:
|
if no_cache:
|
||||||
_compose_with_retry("build", "--no-cache", compose_file=compose_path)
|
_compose_with_retry("build", "--no-cache", compose_file=compose_path)
|
||||||
_compose_with_retry("up", "--build", "-d", compose_file=compose_path)
|
_compose_with_retry("up", "--build", "-d", compose_file=compose_path)
|
||||||
|
|||||||
Reference in New Issue
Block a user