core: GPU containers, bind sub-paths, and stop containers on unit stop

- container launcher: `gpus:` passes through as docker `--gpus`.
- requires: `path:` is appended to the bound URL, for consumers that want a
  sub-path as their base (e.g. an OpenAI-compatible server's /v1).
- container services now get `ExecStop=-docker stop wildpc-<name>`. SIGTERM
  only reached the `docker run` client, so a stop/restart left the container
  running unsupervised and holding its name, and the next start failed with
  a name conflict until systemd gave up.
This commit is contained in:
2026-10-16 03:10:01 -07:00
parent 8a9843658c
commit 55c2f269ec
6 changed files with 90 additions and 4 deletions

View File

@@ -697,7 +697,7 @@ def _requires_env(config: WildpcConfig, dep: DeploymentSpec) -> dict[str, str]:
if r.kind == "deployment" and r.bind:
url = _target_url(config, r.ref)
if url:
out[r.bind] = url
out[r.bind] = url.rstrip("/") + r.path if r.path else url
return out
@@ -787,7 +787,7 @@ def _registry_requires(dep: DeploymentSpec) -> list[dict]:
"""A deployment's `requires` as plain dicts for the registry — carried into the
mesh payload so peers can draw cross-node consumption."""
return [
{"kind": r.kind, "ref": r.ref, "bind": r.bind}
{"kind": r.kind, "ref": r.ref, "bind": r.bind, "path": r.path}
for r in (getattr(dep, "requires", None) or [])
]
@@ -1092,6 +1092,8 @@ def _build_run_cmd(
# certs/data/secrets readable with no chown). ${uid}/${gid} expand
# to the wildpc process's own ids.
cmd.extend(["--user", _subst(run.user, placeholders)]) # type: ignore[union-attr]
if run.gpus: # type: ignore[union-attr]
cmd.extend(["--gpus", run.gpus]) # type: ignore[union-attr]
for tp in run.tmpfs: # type: ignore[union-attr]
cmd.extend(["--tmpfs", _subst(tp, placeholders)])
for container_port, host_port in run.ports.items(): # type: ignore[union-attr]
@@ -1158,10 +1160,16 @@ def _build_stop_cmd(name: str, run: object, source_dir: Path | None) -> list[str
"""The ExecStop teardown command for a runner, or [] if a plain SIGTERM suffices.
Compose stacks need an explicit ``down`` so networks/anonymous volumes are
reclaimed rather than left dangling when the unit stops.
reclaimed rather than left dangling when the unit stops. A container needs an
explicit ``stop``: SIGTERM only reaches the ``docker run`` client, leaving the
container running unsupervised and holding its name, so the next start fails.
``-`` tells systemd a container that's already gone isn't a failure.
"""
if run.launcher == "compose": # type: ignore[union-attr]
return [*_compose_base(name, run, source_dir), "down"]
if run.launcher == "container": # type: ignore[union-attr]
runtime = shutil.which("docker") or shutil.which("podman") or "docker"
return [f"-{runtime}", "stop", f"wildpc-{name}"]
return []

View File

@@ -97,6 +97,9 @@ class RunContainer(LaunchBase):
# tmpfs mounts (e.g. ["/var/run/postgresql"]) for image runtime dirs that must
# be writable when the container runs as a non-default uid.
tmpfs: list[str] = Field(default_factory=list)
# GPUs to hand the container (docker `--gpus`), e.g. "all" or "device=0".
# None → no GPU access.
gpus: str | None = None
class RunNode(LaunchBase):
@@ -308,6 +311,9 @@ class Requirement(BaseModel):
kind: Literal["system", "deployment", "tool"] = "deployment"
ref: str
bind: str | None = None
# Appended to the target's URL when binding — for a consumer that wants a
# sub-path of it (e.g. an OpenAI-compatible server's "/v1") as its base URL.
path: str | None = None
# ---------------------