Runnable-image delivery (kubelet-pullable, the WASI-style pull path)¶
brewlet push --format=image publishes a kubelet-pullable OCI image that the Brewlet
shim runs with the node-resident JDK. This page documents the delivery contract referenced by
SPECIFICATION §4.4. It answers the question "how does a
runtimeClassName: brewletpod name the app as itsimage:and let kubelet pull it, exactly like aruntimeClassName: wasmtimepod names a Wasm module?"
1. TL;DR¶
- The native Brewlet artifact is registry-native but not runnable by containerd.
Its custom layer media types (
application/vnd.brewlet.jar.layer.v1+jar,…classpath.layer.v1+tar,…modulepath.layer.v1+tar) are not among the media types containerd's CRI differ can unpack (tar,tar+gzip,tar+zstd). A pod that setsimage: <native-artifact-ref>therefore fails to pull (ImagePullBackOff); the payload has to reach the node out of band, such as withctr images import. - Runnable-image mode fixes this without changing the native format.
brewlet push --format=imagepublishes the same JAR as a standard, kubelet-pullable OCI image. containerd/kubelet pull + unpack it with no special configuration; the shim recognizes it and runs it on the node-resident JDK. - The developer experience becomes the WASI/SpinKube one:
image: <ref>+runtimeClassName: brewletand nothing else. kubelet pulls, containerd unpacks, the shim launchesjava -jarunder the pod's cgroup. - Runnable-image mode is now the default.
brewlet push(andmvn brewlet:push) produce a runnable image unless you opt into--format=artifact(-Dbrewlet.format=artifact). It is the delivery path that fulfils the WASI/SpinKube parity goal, so it is the out-of-the-box behaviour; the native artifact remains an opt-in choice for registry-native / pre-puller flows.
2. Why the native artifact can't be a pod image:¶
containerd's CRI PullImage unpacks every layer into the snapshotter before the
runtime shim's Create runs. Unpacking dispatches on the layer's media type, and the
differ only understands tar, tar+gzip, and tar+zstd. Brewlet's native layers use
bespoke +jar/+tar media types so the artifact stays self-describing and
registry-native — but that is precisely what makes crictl/kubelet unable to unpack
them. The pull fails long before the shim is ever consulted.
That is fine for a registry + out-of-band node delivery model, but it means the pod
cannot name the artifact as its image. Tiers 8 and 9 of the e2e suite work around
this by importing the artifact straight into the node content store and giving the pod
a busybox placeholder image plus brewlet.sh/artifact-* annotations — proving the
runtime, but not the image: <ref> promise.
3. What --format=image publishes¶
# Same JAR, same launch contract — published as a runnable OCI image.
brewlet push ./target/app.jar registry.example.com/team/app:1.4.2 --format=image
The result is an ordinary OCI image index (multi-arch) whose per-arch manifests are plain OCI images:
| Piece | What it is |
|---|---|
| Manifest media type | application/vnd.oci.image.manifest.v1+json |
| Index media type | application/vnd.oci.image.index.v1+json (multi-arch) |
| Config | application/vnd.oci.image.config.v1+json — a real image config whose rootfs.diff_ids are the sha256 of the uncompressed layer tars |
| Layers | application/vnd.oci.image.layer.v1.tar+gzip — standard, unpackable |
| Launch config | the §4.2 launch descriptor, carried verbatim in the manifest annotation brewlet.sh/jvm-config |
| Layer roles | each layer tagged with brewlet.sh/layer = app | classpath | modulepath |
Layer layout:
- app layer — a flat tar containing the main JAR (named per
mainJar) plus an optional AppCDS.jsa. - classpath / modulepath layers — the same flat-JAR tars a native artifact would ship for layered classpath / JPMS deployments, just gzip-compressed and role-tagged.
Multi-arch by default. A portable bytecode JAR is published for amd64 + arm64
(identical layers, per-arch config differing only in architecture) so any provisioned
node matches. A JAR carrying native libraries narrows this with --arch amd64,arm64
(see multi-arch.md).
OCI correctness note. A layer descriptor's
digestis the sha256 of the gzipped blob, but the image config'srootfs.diff_ids[i]must be the sha256 of the uncompressed tar. Getting this wrong makes containerd reject the image on unpack. The writer computes both; a unit test asserts the diff-ids equal the uncompressed digests.
4. How the shim runs it¶
On the node the shim distinguishes the two formats by the manifest: the presence of the
brewlet.sh/jvm-config annotation ⇒ runnable image (otherwise ⇒ native artifact, the
raw-blob path, unchanged). For a runnable image the shim:
- follows the image index to the node's platform manifest (by
GOARCH); - decodes the launch config from
brewlet.sh/jvm-config; - gunzips the app layer to recover the JAR (and any
.jsa), and gunzips each classpath/modulepath layer to a temporary tar; - feeds those tars to the existing
StageClasspathLayers/StageModulepathLayersbundle-assembly path — so runnable images and native artifacts converge on the samejava -jar/-cp/-p -msandbox on the node-resident JDK, under the pod's cgroup limits.
Nothing about JVM launch, cgroup-awareness, JDK/launcher selection, or Brewlet's overlay rootfs (shared read-only JDK lower + per-container upper) changes.
5. Operator & webhook: no change required¶
- The
JavaApplicationcontroller already sets the Deployment's containerimage:tospec.artifact.image. With a runnable image that ref is now pullable, so the happy path just works. - The admission webhook still stamps
brewlet.sh/artifact-ref+brewlet.sh/artifact-digestfrom the (digest-pinned) ref; for a runnable image the digest is the image-index digest, which the shim resolver follows to the platform manifest.
6. When to use which¶
| Runnable image (default) | Native artifact (--format=artifact) |
|
|---|---|---|
| Media types | standard OCI tar+gzip |
custom +jar / +tar |
Pod image: <ref> pulls via kubelet |
✓ | ✗ (needs out-of-band delivery) |
| Registry-native / smallest | slightly larger (OS-image framing) | ✓ |
| Node delivery | kubelet PullImage, like any image |
pre-puller / import |
| Developer UX | pure WASI-style image: <ref> |
ref + node delivery |
The default runnable image gives the pure image: <ref> experience end to end — the
WASI/SpinKube parity goal. Opt into --format=artifact when you have (or are building) a
node pre-puller and want the leanest registry footprint / self-describing media types.
7. End-to-end behavior¶
The runnable-image tier of the e2e suite provisions a real kind/CI node,
brewlet push --format=images the demo JAR, imports it into the node's k8s.io
content store, and asserts ctr images unpack SUCCEEDS — the exact operation that
ImagePullBackOffs for a native artifact. It then runs a runtimeClassName: brewlet
Deployment whose container image: is the brewlet ref itself (no placeholder,
imagePullPolicy: Never) and asserts the pod is Ready with that image, serves a 200
from /hello, and that the JVM is cgroup-aware (availableProcessors == 1, bounded
maxMemory).