Skip to content

Run unmodified task images: guest injected as an OCI image volume, plus second-runtime layers - #70

Open
Tomer Glottmann (tomergee) wants to merge 2 commits into
mainfrom
template-task-image
Open

Tomer Glottmann (tomergee) wants to merge 2 commits into
mainfrom
template-task-image

Conversation

@tomergee

Copy link
Copy Markdown
Collaborator

What

Run any digest-pinned OCI image as an ate-env environment without rebuilding it. The task image stays as published; the ate-env-guest is mounted into it as a read-only OCI image volume at /ate and started from there. Two commits:

1. Task images (3f943a8)

  • CreateEnvironment gains an image field. ate-env-api takes the request's template (default default-template) as the base, derives a template named <base>-<12 hex of the digest> next to it on first use, reuses it afterwards, and reports it in the response. Concurrent first creates on the same image both succeed.
  • ate-env manifest template --task-image writes such a template up front; --workspace passes -workspace to the guest.
  • CLI ate-env create --image, Python client.create(image=).
  • Guard rails: unpinned image, derived name over 63 characters, or a base command that is not the guest binary are InvalidArgument before anything is created; missing base or a derived name held by a template running another image are FailedPrecondition.
  • The derivation lives in internal/apiservice/imagetemplate.go and accepts either kind of base (guest as image, or already injected).

2. A second runtime as a layer (a76974d)

  • ate-env-guest gains -sidecar (start and supervise an extra runtime from a layer: no shell, output logged with a prefix, restart with backoff 1s→30s) and -sidecar-readyz (URLs the guest's /readyz, Substrate's wakeup probe, waits for once).
  • ate-env manifest template gains --layer name=image@sha256:...=/mount, --sidecar, --sidecar-readyz. Layers survive derivation, so create --image on a layered base inherits the second runtime.

This is step 1 of the runtime-injection plan: the mechanism every injected runtime needs. The intended first foreign runtime is OpenSandbox's execd, which is built to be injected this way.

Docs

Testing

  • go test ./...: 11 packages, including derivation from both base kinds, naming limits, every rejection, server create/reuse/race/failure paths against the fake control plane, manifest layers and sidecars, and the guest's sidecar supervisor (restart with backoff, stop on shutdown, readiness gate once per URL).
  • clients/python unit tests: 59 pass. tests/e2e/test_full_stack.py gains two cheap negative tests and a create-from-image test gated on ATE_ENV_TASK_IMAGE that checks the guest comes from the volume and not the image, the rootfs is the image's, files round-trip, and a second environment reuses the derived template.
  • On a Kubernetes cluster running Substrate, fresh two-worker ate-env built from this branch:
    • ate-env create --image of an unmodified python:3.12-slim: created in 1 s, derived template present, python3 --version answered from the image's own rootfs with the guest under /ate, deleted; 44 s end to end on a cold worker.
    • examples/runtime-layers/demo.sh: layered template, environment serving in 3 s, the second runtime answered from inside, then a debian:bookworm-slim environment created on the same base with --image did the same; 23 s total.
    • A first attempt with the dynamically linked busybox:1.37 failed on the debian image with a glibc version error, exactly as the supervisor and fail-fast readiness are meant to surface it. The docs now state the self-contained-layer rule and that signature.
  • hack/verify/boilerplate.sh passes. Go stubs via buf generate with protoc-gen-go v1.36.11; Python stubs via grpcio-tools 1.61.3, matching the committed generator and keeping the license headers.

Known gaps, by design

  • The task image is not checked for existence at create time; a bad reference shows as the actor failing to start.
  • Derived templates and their golden snapshots are not garbage collected.
  • A second runtime is reachable from processes inside the environment now. Outside clients reach only port 80: the router forwards there unless a CONNECT authority names another port, and ate-env-api proxies only the guest. An endpoint lookup that names a runtime's port is the next piece and belongs with the OpenSandbox backend.
  • Pre-existing and unchanged: CreateEnvironment resolves the template in the environment's atespace and ignores template.atespace.

Relation to other PRs

Until now a template's container image had to be the ate-env-guest image
itself, so running a task image meant rebaking it with the guest as the
entrypoint, once per image. Substrate's image volumes allow the other
split: the task image stays unmodified and the guest is mounted into it
read-only and started from the mount.

Two ways to get such a template:

- ate-env manifest template --task-image <repo@sha256:...> writes a
  template whose container is the task image, with the guest image as a
  volume at /ate and the command re-rooted to /ate/ko-app/ate-env-guest.
  --workspace passes -workspace to the guest in either mode.
- CreateEnvironment gains an image field. ate-env-api takes the request's
  template (default-template by default) as the base, derives
  "<base>-<12 hex digits of the digest>" from it on first use and reuses
  it for later environments on the same image; the response reports it.
  A derived name already taken by a template that runs something else is
  a FailedPrecondition, as is a missing base; an unpinned image, a
  derived name over 63 characters, or a base command that is not an
  absolute path to the guest is an InvalidArgument. Concurrent creates
  racing on one derived template both succeed.

The derivation lives in internal/apiservice/imagetemplate.go and accepts
either kind of base, guest-as-image or already injected, so a second
runtime can reuse it. internal/ate gains Get/CreateActorTemplate and the
fake control plane stores templates. The CLI gains create --image, the
Python client create(image=), and the full-stack e2e suite a
create-from-image test gated on ATE_ENV_TASK_IMAGE that checks the guest
comes from the volume, the rootfs is the image's, and a second
environment reuses the derived template.

docs/task-images/README.md is the how-to (both flows, what an image
needs, errors, cleanup) and docs/task-images/DESIGN.md the design
(derivation rules, naming and idempotency, failure semantics, security,
alternatives, future work). Stubs regenerated: Go with buf and
protoc-gen-go v1.36.11, Python with grpcio-tools 1.61.3, keeping the
committed license headers.
A task-image template carries the guest as a read-only image volume; an
agent framework's own in-sandbox daemon, such as OpenSandbox's execd, is
one more volume plus a process that has to start next to the guest. The
guest is the container's only process, so it is the one that starts it.

ate-env-guest gains -sidecar (repeatable: an absolute binary path and its
arguments, run without a shell, output logged with the binary's name as
prefix, restarted with a backoff that doubles from one second to thirty
and resets after a minute of stable running) and -sidecar-readyz
(repeatable: URLs that must answer 2xx once before /readyz reports
ready, so Substrate's wakeup probe admits traffic only when every runtime
is serving). Bad -sidecar values fail at start rather than loop.

ate-env manifest template gains --layer name=image@sha256:...=/mount,
--sidecar and --sidecar-readyz, which become image volumes, mounts and
guest flags on the template. Template derivation recognizes the guest
volume by name rather than as "an image volume", so a base with layers
still gets the guest added or kept correctly and environments created on
demand with --image inherit the second runtime. Only a command that
starts with the guest binary is re-rooted under the mount.

docs/task-images/RUNTIMES.md describes the model, the flags, who can
reach the second runtime from where (processes inside the environment
now; outside callers still only reach port 80 through the router, which
is the next piece), suspend and restart behavior, and the limits.
examples/runtime-layers is a runnable demo with a static busybox as the
stand-in runtime: httpd on 44772 as the sidecar under both a python and
a debian task image, the second one created from an image on the layered
base. The first attempt with the dynamically linked busybox failed on
the debian image with a glibc version error, which is why the docs make
the self-contained-layer rule and its failure signature explicit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant