A Rust reimplementation of containerd's kubelet-facing
surface: a daemon that serves the Kubernetes CRI v1 API (RuntimeService +
ImageService) over a Unix socket and runs containers via crun (a fast,
runc-compatible OCI runtime). The goal (SC-001) is to be a drop-in replacement
for containerd as the node container runtime and pass the Kubernetes
Conformance suite.
Status: a single-node cluster brought up with kubeadm runs entirely on
containerd-rs — control plane converges, the node reaches Ready, the system
pods (etcd, apiserver, controller-manager, scheduler, kube-proxy, CoreDNS) run,
and kubectl exec/attach/port-forward work over SPDY. A [Conformance] test
passes end-to-end; the [Conformance] suite runs in CI, split into per-sig
workflows (see below).
containerd-rs uses a direct-crun model: the daemon shells out to crun and
supervises the container process in-process (no TTRPC shim). The CRI plugin,
content store, snapshotter, and image puller are all built in.
kubelet ──CRI v1 gRPC (unix socket)──▶ containerd-rs daemon
│
Exec/Attach/PortForward ── URL ─────▶ streaming server (SPDY/3.1 + WS)
│
images ──▶ content store (verify-on-commit) + overlayfs snapshots
metadata ──▶ redb │
pods ──▶ CNI (Flannel) / host-net ▼
crun (one process per container)
| Crate | Responsibility |
|---|---|
core-types |
Shared types: content digests, OCI descriptors, namespaces |
content |
Content-addressable blob store (digest/size verified on commit, dedup) |
metadata |
Persistent metadata store backed by redb |
snapshots |
Overlayfs snapshotter: layer diff apply, OCI whiteouts, chainIDs |
images |
Image pull pipeline (oci-client), registry auth, chainID identity, GC |
runtime |
OCI bundle generation + crun supervision (run/exec/kill/stats) |
sandbox |
Pod sandbox model: network namespace + CNI plugin chain |
cri |
CRI v1 gRPC server + the exec/attach/port-forward streaming server |
containerd-rs |
The daemon binary (config, store bring-up, serve, reconcile) |
See docs/architecture.md for the request flow and design
notes, and docs/configuration.md for the full config
reference.
make build # debug build of the workspace
make release # release build (produces target/release/containerd-rs)
make check # fmt-check + clippy (-D warnings) + tests — the local gatemake check mirrors CI exactly; run it before pushing.
sudo containerd-rs --config /etc/containerd-rs/config.tomlA minimal config (all fields have defaults — see the config reference):
root = "/var/lib/containerd-rs" # persistent store (content, snapshots, metadata)
state = "/run/containerd-rs" # ephemeral state (crun root, OCI bundles)
cri_socket = "/run/containerd-rs.sock"
stream_server_address = "127.0.0.1:10010"
[cri]
sandbox_image = "registry.k8s.io/pause:3.10"
snapshotter = "overlayfs"
cni_conf_dir = "/etc/cni/net.d"
cni_bin_dir = "/opt/cni/bin"Point the kubelet at it with --container-runtime-endpoint=unix:///run/containerd-rs.sock.
Smoke-test with crictl:
crictl --runtime-endpoint unix:///run/containerd-rs.sock version
crictl --runtime-endpoint unix:///run/containerd-rs.sock imagesci/kubeadm-init.sh (via make cluster-up) stands up a kubeadm control plane on
containerd-rs as the sole runtime. Requires root + crun, kubeadm/kubelet/
kubectl, crictl, and CNI plugins on the host.
make release
make cluster-up # phase-by-phase kubeadm + manual kubelet; waits for Ready + 7/7 pods
make cluster-down # kubeadm reset + stop kubelet/daemonThe Kubernetes [Conformance] suite is the acceptance gate. It runs via
hydrophone:
make conformance-smoke # one Conformance test (fast pipeline check)
make conformance # full [Conformance] suite -> conformance-results/In CI the suite is split into manual (workflow_dispatch) per-sig workflows,
each building the daemon, installing the Kubernetes toolchain, bringing up the
cluster on a real runner, and running its slice via hydrophone (they share
conformance-reusable.yml). The split covers 441 of the [Conformance]
specs across nine SIGs (registry.k8s.io/conformance:v1.35.6):
| Workflow | Focus | Specs | Asserts |
|---|---|---|---|
conformance-sig-node.yml |
[sig-node] |
105 | runtime/CRI on the node: lifecycle, exec/attach, probes, security context, env, sysctls, ephemeral containers |
conformance-sig-api-machinery.yml |
[sig-api-machinery] |
95 | apiserver contract: CRDs, admission webhooks, watch, namespaces, garbage collection, resource quota, server-side apply |
conformance-sig-storage.yml |
[sig-storage] |
91 | volume/mount path: emptyDir, configMap/secret/projected/downwardAPI volumes, subpaths |
conformance-sig-apps.yml |
[sig-apps] |
60 | workload controllers: Deployment, ReplicaSet, StatefulSet, DaemonSet, Job, CronJob (rolling updates, scale, ordered bring-up) |
conformance-sig-network.yml |
[sig-network] |
47 | pod networking, Services/ClusterIP, DNS, hostPort |
conformance-sig-cli.yml |
[sig-cli] |
17 | kubectl behaviours: create/apply/run/expose/patch/label |
conformance-sig-scheduling.yml |
[sig-scheduling] |
11 | predicates and basic scheduling: node selectors, taints/tolerations, resource fit |
conformance-sig-auth.yml |
[sig-auth] |
10 | ServiceAccount tokens, projected SA volumes, related authn/authz |
conformance-sig-instrumentation.yml |
[sig-instrumentation] |
4 | Events API lifecycle: create/patch/delete/list |
A few [Conformance] specs structurally require a multi-node cluster (they
fail with "needs a cluster with at least 2 nodes") and so cannot pass on this
single-node setup — [sig-architecture] … should have at least two untainted nodes (no workflow) and [sig-apps] Daemon set [Serial] should rollback without unnecessary restarts. ci/run-conformance.sh skips these by default (SKIP
regex, overridable); they are environmental, not runtime, limitations.
They run only on demand (conformance is expensive; CI minutes are limited), and
each can be validated locally first with the docker harness, e.g.
make conformance-docker FOCUS='\[sig-apps\].*\[Conformance\]'. The status
badges at the top of this README reflect each workflow's latest run.
Alongside the Kubernetes suite, containerd-rs is gated on critest — the CRI-conformance suite that upstream containerd and CRI-O themselves gate on. It exercises the CRI contract directly (no kubelet), so it catches runtime-surface regressions the end-to-end suite would only find indirectly.
Equivalence with containerd is defined as: critest green minus a ratified, documented skip list. The current local result is 85 Passed / 0 Failed / 28 Skipped — every runnable spec passes. The skips are by-design or environmental, not runtime gaps (containerd maintains its own skips likewise):
| Skipped spec | Why |
|---|---|
AppArmor (profile tests) |
Environmental: the profile can't be loaded on the harness (in-container → apparmor_parser//sys/kernel/security/apparmor unavailable, matching containerd's HostSupports() guard). RuntimeDefault AppArmor is a no-op where AppArmor is unsupported. |
should output OOMKilled reason |
By-design: crun run deletes the container cgroup on exit, so the post-mortem OOM flag isn't readable. |
should support RunAsUserName |
Windows-style username; N/A on Linux. |
Run it locally with no CI minutes / host sudo (self-contained privileged
container — installs critest, brings up the daemon, runs the suite):
make critest-docker # full suite (green-minus-skips)
make critest-docker FOCUS='seccomp default' # one focused specThe critest.yml workflow runs the same gate in CI (workflow_dispatch).
- Unit + contract + integration tests:
make test(orcargo test --workspace). - Tests that need
crun/network are#[ignore]d; run them withcargo test -p <crate> -- --ignored. - CRI contract tests:
crates/cri/tests/contract_*.rs. - End-to-end pod lifecycle + image management:
crates/cri/tests/integration_*.rs.
MIT — see LICENSE.