Skip to content

Commit 77fa6a6

Browse files
committed
docs: runnable demo walkthrough and README quickstart
demo/run-demo.sh walks the whole story against throwaway local containers: build three labeled variants of a runnable hello image, publish with `variant push`, show the index artifact and per-profile rankings, pull under mocked CUDA-12.8 and CPU-only profiles (the container prints which variant it is), then repeat via variant-proxy with plain `docker push` only. Profiles live in demo/profiles/ and double as ready-made --properties-file inputs. Makefile gains build / install-plugin / demo targets; README is rewritten with the concept mapping, command table, registry deployment modes, and quickstart. Claude-Session: https://claude.ai/code/session_01D383U8kkQkJc1yzyC5H5Nk
1 parent 203f058 commit 77fa6a6

8 files changed

Lines changed: 249 additions & 2 deletions

File tree

Makefile

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
GO ?= go
2+
PLUGIN_DIR ?= $(HOME)/.docker/cli-plugins
23

3-
.PHONY: test vet fmt tidy e2e
4+
.PHONY: build install-plugin demo test vet fmt tidy e2e
5+
6+
build:
7+
$(GO) build -o bin/docker-variant ./cmd/docker-variant
8+
$(GO) build -o bin/variant-proxy ./cmd/variant-proxy
9+
10+
install-plugin: build
11+
mkdir -p $(PLUGIN_DIR)
12+
install bin/docker-variant $(PLUGIN_DIR)/docker-variant
13+
14+
demo:
15+
./demo/run-demo.sh
416

517
e2e:
618
./e2e/run.sh

README.md

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,105 @@
11
# docker-variant
2-
Docker extension with PEP 817-like variant properties matching for container images
2+
3+
Hardware-variant-aware container image selection for Docker, modeled on
4+
Python's [PEP 817 wheel variants](https://peps.python.org/pep-0817/).
5+
6+
Publish one *base version* of an image as several *variants* — say a CUDA
7+
12.8 build, a CUDA 12.6 build, and a CPU-only fallback — and let the client
8+
pick the right one for the machine it runs on:
9+
10+
```console
11+
$ docker variant pull registry.example.com/myorg/app:2.1.0
12+
Selected variant "cu128" of registry.example.com/myorg/app version 2.1.0
13+
...
14+
Tagged registry.example.com/myorg/app:2.1.0 and registry.example.com/myorg/app:2.1.0-cu128
15+
```
16+
17+
The same command on a GPU-less machine selects the `null` (CPU-only)
18+
variant, and on a machine with no compatible variant at all it falls back to
19+
the plain `2.1.0` tag.
20+
21+
## How it works
22+
23+
PEP 817's wheel-variant concepts are mapped onto standard Docker/OCI
24+
primitives — no registry modifications required:
25+
26+
| PEP 817 | docker-variant |
27+
|---|---|
28+
| variant label in the wheel filename | tag suffix: `app:2.1.0-cu128` |
29+
| variant properties (`ns :: feature :: value`) | image config labels: `dev.pep817.variant.nvidia.cuda_version_lower_bound=12.8` |
30+
| `{name}-{version}-variants.json` on the index | variant index: an OCI artifact at `app:2.1.0-variants` mapping labels → properties + manifest **digests** |
31+
| provider plugins (hardware detection) | compiled-in detectors (NVIDIA via nvidia-smi, x86-64 levels via CPUID, arm64) |
32+
| null variant | `app:2.1.0-null`, zero properties, always compatible, ranked last |
33+
34+
Declaring a variant is just Dockerfile labels:
35+
36+
```dockerfile
37+
LABEL dev.pep817.variant-label="cu128" \
38+
dev.pep817.variant.nvidia.cuda_version_lower_bound="12.8"
39+
```
40+
41+
`docker variant pull` fetches the index, detects the local hardware, ranks
42+
the compatible variants (index priorities → system preference →
43+
deterministic tie-breaks), and pulls the winner **by digest**.
44+
45+
## Commands
46+
47+
| Command | What it does |
48+
|---|---|
49+
| `docker variant detect` | Show this machine's variant properties |
50+
| `docker variant pull REPO:VERSION` | Pull the best-matching variant (`--dry-run`, `--properties-file`, `--no-fallback`) |
51+
| `docker variant push REPO:VERSION-LABEL` | Push a variant tag and update the repository's variant index |
52+
| `docker variant list REPO:VERSION` | Table of variants with match ranking for this host |
53+
| `docker variant inspect REPO:VERSION` | Raw variant index JSON |
54+
| `docker variant index update REPO:VERSION` | Rebuild the index by scanning the registry's tags |
55+
56+
All registry-touching commands accept `--plain-http`; loopback registries
57+
use plain HTTP automatically.
58+
59+
## Registry side
60+
61+
Two deployment modes:
62+
63+
1. **Any V2 registry** (Docker Hub, registry:2, Harbor, …): `variant push` /
64+
`index update` maintain the `-variants` index artifact from the client.
65+
2. **variant-proxy**: a stateless reverse proxy in front of the registry
66+
that serves `GET /v2/<name>/_variants/<version>` computed on the fly from
67+
image labels — publishers then need nothing but plain `docker push`.
68+
The client tries the endpoint first and falls back to the artifact.
69+
70+
```console
71+
$ variant-proxy --listen :5599 --upstream registry.internal:5000
72+
```
73+
74+
## Try it
75+
76+
```console
77+
$ make demo # full walkthrough against a throwaway local registry
78+
$ make install-plugin # installs into ~/.docker/cli-plugins => `docker variant ...`
79+
$ make e2e # the same flows as CI assertions
80+
```
81+
82+
The demo builds three variants of a runnable hello image, publishes them,
83+
then pulls under three mocked hardware profiles (`demo/profiles/*.json`) so
84+
you can watch the selection change — and repeats the flow through
85+
variant-proxy with plain `docker push` only.
86+
87+
## Development
88+
89+
```console
90+
$ make test # unit tests (pkg/variant is pure and I/O-free)
91+
$ make vet
92+
$ make e2e # needs docker
93+
```
94+
95+
Design and rationale: [docs/PLAN.md](docs/PLAN.md) (roadmap, plugin-vs-fork
96+
and registry-side decisions) and [docs/DESIGN.md](docs/DESIGN.md) (frozen v1
97+
contracts: label schema, index schema, selection algorithm, ADRs).
98+
99+
## Status / roadmap
100+
101+
Working end-to-end (see `e2e/run.sh`): publish, list, select, pull,
102+
fallback chain, proxy. Planned next: OCI 1.1 referrers-based index
103+
attachment, index signing (cosign), an exec-based detection-provider
104+
protocol behind explicit opt-in, and a moby fork PoC to demonstrate native
105+
`docker pull` integration.

demo/Dockerfile.gpu

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# A GPU variant of the demo app: the variant label and its property are
2+
# plain Dockerfile LABELs — no special tooling needed at build time.
3+
FROM busybox
4+
ARG VARIANT
5+
ARG CUDA_LOWER_BOUND
6+
LABEL dev.pep817.variant-label="${VARIANT}"
7+
LABEL dev.pep817.variant.nvidia.cuda_version_lower_bound="${CUDA_LOWER_BOUND}"
8+
RUN printf 'echo "Hello from demo-app 1.0.0, variant: %s (built for CUDA >= %s)"\n' \
9+
"${VARIANT}" "${CUDA_LOWER_BOUND}" > /hello.sh
10+
CMD ["/bin/sh", "/hello.sh"]

demo/Dockerfile.null

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# The null variant: a safe CPU-only fallback with zero variant properties.
2+
FROM busybox
3+
LABEL dev.pep817.variant-label="null"
4+
RUN printf 'echo "Hello from demo-app 1.0.0, variant: null (CPU-only fallback)"\n' > /hello.sh
5+
CMD ["/bin/sh", "/hello.sh"]

demo/profiles/cpu-only.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"x86_64": { "level": ["v3", "v2", "v1"] }
3+
}

demo/profiles/gpu-cuda126.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"nvidia": { "cuda_version_lower_bound": ["12.6", "12.4", "12.2", "12.0"] },
3+
"x86_64": { "level": ["v3", "v2", "v1"] }
4+
}

demo/profiles/gpu-cuda128.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"nvidia": { "cuda_version_lower_bound": ["12.8", "12.6", "12.4", "12.2", "12.0"] },
3+
"x86_64": { "level": ["v3", "v2", "v1"] }
4+
}

demo/run-demo.sh

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#!/usr/bin/env bash
2+
# Interactive walkthrough of docker-variant: publishes three variants of a
3+
# runnable demo app to a local registry, then shows how `variant pull`
4+
# selects a different image per (mocked) hardware profile — first with
5+
# client-maintained indexes, then with the server-computed variant-proxy.
6+
#
7+
# Requires: docker, the Go toolchain, curl. Everything runs against
8+
# disposable local containers; pass --keep to leave them running for
9+
# exploration.
10+
set -euo pipefail
11+
12+
cd "$(dirname "$0")/.."
13+
REG_PORT="${DEMO_REGISTRY_PORT:-5595}"
14+
PROXY_PORT="${DEMO_PROXY_PORT:-5596}"
15+
REGISTRY="127.0.0.1:${REG_PORT}"
16+
PROXY="127.0.0.1:${PROXY_PORT}"
17+
REPO="${REGISTRY}/demo/app"
18+
PROXY_REPO="${PROXY}/demo/proxied-app"
19+
CONTAINER="variant-demo-registry"
20+
KEEP=false
21+
[ "${1:-}" = "--keep" ] && KEEP=true
22+
23+
BIN=bin/docker-variant
24+
PROXY_BIN=bin/variant-proxy
25+
PROXY_PID=""
26+
27+
say() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }
28+
run() { printf '\033[0;33m$ %s\033[0m\n' "$*"; "$@"; }
29+
30+
cleanup() {
31+
$KEEP && { echo; echo "(--keep: registry ${REGISTRY} and proxy ${PROXY} left running)"; return; }
32+
[ -n "${PROXY_PID}" ] && kill "${PROXY_PID}" >/dev/null 2>&1 || true
33+
docker rm -f "${CONTAINER}" >/dev/null 2>&1 || true
34+
docker rmi -f \
35+
"${REPO}:1.0.0" "${REPO}:1.0.0-cu128" "${REPO}:1.0.0-cu126" "${REPO}:1.0.0-null" \
36+
"${PROXY_REPO}:1.0.0" "${PROXY_REPO}:1.0.0-cu128" "${PROXY_REPO}:1.0.0-null" >/dev/null 2>&1 || true
37+
}
38+
trap cleanup EXIT
39+
40+
say "Building docker-variant and variant-proxy"
41+
run make build
42+
43+
say "Starting a throwaway registry at ${REGISTRY}"
44+
docker rm -f "${CONTAINER}" >/dev/null 2>&1 || true
45+
run docker run -d --name "${CONTAINER}" -p "127.0.0.1:${REG_PORT}:5000" registry:2
46+
for _ in $(seq 1 30); do curl -fsS "http://${REGISTRY}/v2/" >/dev/null 2>&1 && break; sleep 0.5; done
47+
48+
say "Building three variants of demo-app 1.0.0 (labels are plain Dockerfile LABELs)"
49+
run docker build -q -f demo/Dockerfile.gpu --build-arg VARIANT=cu128 --build-arg CUDA_LOWER_BOUND=12.8 \
50+
-t "${REPO}:1.0.0-cu128" demo
51+
run docker build -q -f demo/Dockerfile.gpu --build-arg VARIANT=cu126 --build-arg CUDA_LOWER_BOUND=12.6 \
52+
-t "${REPO}:1.0.0-cu126" demo
53+
run docker build -q -f demo/Dockerfile.null -t "${REPO}:1.0.0-null" demo
54+
55+
say "Publishing them with 'variant push' (pushes the tag + updates the variant index artifact)"
56+
run "${BIN}" variant push "${REPO}:1.0.0-cu128"
57+
run "${BIN}" variant push "${REPO}:1.0.0-cu126"
58+
run "${BIN}" variant push "${REPO}:1.0.0-null"
59+
60+
say "The variant index is a plain OCI artifact at ${REPO}:1.0.0-variants"
61+
run "${BIN}" variant inspect "${REPO}:1.0.0"
62+
63+
say "What this machine actually looks like"
64+
run "${BIN}" variant detect || true
65+
66+
say "Ranking under three hardware profiles (mocked with --properties-file)"
67+
for profile in gpu-cuda128 gpu-cuda126 cpu-only; do
68+
echo
69+
echo "--- profile: ${profile} ---"
70+
run "${BIN}" variant list "${REPO}:1.0.0" --properties-file "demo/profiles/${profile}.json"
71+
done
72+
73+
say "'variant pull' on a CUDA 12.8 machine"
74+
run "${BIN}" variant pull "${REPO}:1.0.0" --properties-file demo/profiles/gpu-cuda128.json
75+
run docker run --rm "${REPO}:1.0.0"
76+
77+
say "'variant pull' on a CPU-only machine (same command, different hardware)"
78+
docker rmi -f "${REPO}:1.0.0" >/dev/null
79+
run "${BIN}" variant pull "${REPO}:1.0.0" --properties-file demo/profiles/cpu-only.json
80+
run docker run --rm "${REPO}:1.0.0"
81+
82+
say "Phase B: variant-proxy computes the index server-side from labels"
83+
run_bg() { printf '\033[0;33m$ %s &\033[0m\n' "$*"; "$@" & }
84+
run_bg "${PROXY_BIN}" --listen "127.0.0.1:${PROXY_PORT}" --upstream "${REGISTRY}"
85+
PROXY_PID=$!
86+
for _ in $(seq 1 30); do curl -fsS "http://${PROXY}/v2/" >/dev/null 2>&1 && break; sleep 0.5; done
87+
88+
echo
89+
echo "Publish through the proxy with NOTHING but plain 'docker push':"
90+
docker tag "${REPO}:1.0.0-cu128" "${PROXY_REPO}:1.0.0-cu128"
91+
docker tag "${REPO}:1.0.0-null" "${PROXY_REPO}:1.0.0-null"
92+
run docker push -q "${PROXY_REPO}:1.0.0-cu128"
93+
run docker push -q "${PROXY_REPO}:1.0.0-null"
94+
95+
echo
96+
echo "The proxy serves the index computed from the image labels:"
97+
run curl -fsS "http://${PROXY}/v2/demo/proxied-app/_variants/1.0.0"
98+
99+
echo
100+
echo "...and 'variant pull' works against it the same way:"
101+
run "${BIN}" variant pull "${PROXY_REPO}:1.0.0" --properties-file demo/profiles/gpu-cuda128.json
102+
run docker run --rm "${PROXY_REPO}:1.0.0"
103+
104+
say "Done"
105+
echo "Tip: 'make install-plugin' installs the binary into ~/.docker/cli-plugins/"
106+
echo "so all of the above works as native 'docker variant ...' commands."

0 commit comments

Comments
 (0)