Skip to content

feat(arc): add an optional pull-through registry cache for dind runners - #65

Merged
Mearman merged 7 commits into
mainfrom
feat/arc-registry-cache
Oct 8, 2026
Merged

Mearman merged 7 commits into
mainfrom
feat/arc-registry-cache

Conversation

@Mearman

@Mearman Mearman commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Adds github_runner_arc_registry_cache_enabled to the github_runner_arc role: a pull-through cache that every Docker-in-Docker scale set pulls through, with nothing to change in workflows, Dockerfiles or jobs. It is off by default, and with it off the role renders exactly what it rendered before (the unit tests compare against the earlier rendering written out in full); turning it off again removes the cache, its storage, the per-namespace ConfigMaps and the written-out dind wiring.

The cache is one Deployment with a CNCF Distribution (registry:3) container per upstream registry in proxy mode, each on its own port of one Service, with persistent (PVC, size and class) or ephemeral (capped emptyDir) storage. Registries are data (github_runner_arc_registry_cache_registries, Docker Hub, ghcr.io and quay.io by default); each can name a Secret in the cache's namespace holding a read-only credential, which the container reads through secretKeyRef, so jobs pull that registry's private images without logging in. Inline credentials are rejected at validation, the role only compares the Secret's key names, and a NetworkPolicy admits only the runner namespaces.

Runner pods use it through mirrors rather than a proxy. dockerd 29 (the chart's docker:dind, which uses the containerd image store) resolves every registry through containerd's ConfigureHosts with /etc/docker/certs.d as its hosts directory (moby daemon/hosts.go), so a hosts.toml per registry listing the cache ahead of the registry covers non-Hub registries too, for the daemon's pulls and for its built-in BuildKit. A docker-container builder created by docker/setup-buildx-action does its own pulls and inherits none of the daemon's settings, but buildx loads buildkitd.default.toml from $BUILDX_CONFIG when buildx create is given no config (buildx builder/builder.go), and setup-buildx-action passes none unless asked (src/context.ts), so the runner container gets BUILDX_CONFIG pointing at a writable directory holding the same mirrors. Both resolvers try the mirror, then the registry when the mirror refuses the connection or answers with an error, which is what makes it fail open. The mirror is also given the referrers capability: dockerd looks up an image's referrers on every pull, and with only pull and resolve containerd sent that lookup straight to the registry, which for a registry that needs a login failed the whole pull even though the cache had served every manifest and blob (the kind test caught this).

The chart's dind container cannot take an extra mount, so with the cache on, a container_mode: dind profile has the chart's own dind wiring (Kubernetes 1.29 and later, dind as a native sidecar) written out in its values and containerMode dropped. A values file that already writes out a dind container just gets the mount.

I tested the mechanisms against real docker:dind 29.8.2 daemons with local pull-through caches, reading each cache's access log for the image's manifest requests (a fresh daemon per row, so nothing came from a local store):

Pull path No config (control) registry-mirrors only Proxy with CA (docker-registry-proxy) hosts.toml + buildkitd.default.toml (this PR)
docker pull, Hub direct cache cache cache
docker pull, ghcr.io direct direct cache cache
legacy builder (DOCKER_BUILDKIT=0), Hub / ghcr.io direct / direct cache / direct not run cache / cache
docker build (buildx, docker driver), Hub / ghcr.io direct / direct cache / direct cache / cache cache / cache
docker buildx build --builder default, Hub / ghcr.io direct / direct cache / direct not run cache / cache
docker-container builder as setup-buildx-action creates it, Hub / ghcr.io direct / direct direct / direct direct / direct cache / cache

Fail-open with the mirror design: with the cache containers stopped, and with them replaced by a server answering every request with 503, every path above still pulled from both registries. A cache that accepts connections and never answers held one pull until containerd's own timeout before it fell back, which is why each cache container has a readiness probe: once it fails, the Service has no endpoints, connections are refused, and pulls fall back at once. The proxy design, by contrast, failed every pull once the proxy was stopped (proxyconnect tcp: ... no route to host), so it would need a pod-local forwarder to fail open, would put a MITM CA in every runner pod, and still would not reach the docker-container builder, whose container environment carried no proxy settings. The mirror design needs no CA, no proxy and no forwarder, so I went with it.

What bypasses the cache (and still works, uncached): registries not in the list; a docker-container builder created with its own --config/buildkitd-config input, or one created with DOCKER_CONFIG/BUILDX_CONFIG changed by the job; Docker run inside a job container rather than through the pod's daemon; a dind daemon switched back to the legacy graph-driver store, where only Docker Hub could be mirrored; and buildx versions that predate the default config file.

Security notes are in the role README: anything reachable from the runner namespaces can pull what the configured credentials can read, so they should be read-only and scoped, and the cache holds those images on its volume.

tests/registry_cache/run.sh (run by the new registry-cache.yml workflow on pull requests) installs the cache as the role renders it in kind, with Docker Hub, ghcr.io and an in-cluster registry behind basic auth whose credential the cache reads from a Secret, starts a pod from the chart's runner template as the role renders a dind profile with the cache on, pulls from all three with docker pull and builds from all three through a docker-container builder created exactly as setup-buildx-action creates one, with no login anywhere, and checks the cache's access log for each; then it scales the cache to zero and checks a second pod still pulls and builds directly. It passes locally on arm64 (kind 0.33, Helm 4). The capabilities test had the same Helm 4 problem reading the rendered chart (Helm 4 prints its pull notice on stdout), fixed in its own commit.

Closes #64.

github_runner_arc_registry_cache_enabled installs a Deployment with one
CNCF Distribution container per upstream registry, in proxy mode, behind
one Service, and points every dind scale set at it without any change on
the workflow side.

dockerd reads a hosts.toml per registry from /etc/docker/certs.d, which
lists the cache as a pull and resolve mirror ahead of the registry, and
buildx reads the same mirrors from buildkitd.default.toml under
BUILDX_CONFIG when it creates a docker-container builder, as
docker/setup-buildx-action does. Both resolvers move on to the registry
itself when the mirror refuses the connection or answers with an error,
so a cache that is down slows a pull rather than failing it.

The chart's dind container cannot take an extra mount, so with the cache
on a dind profile has the chart's dind wiring written out in its values.
Credentials are only read from Secrets in the cache's namespace, and a
NetworkPolicy admits only the runner namespaces. With the cache off the
values render exactly as before and the run removes what it added.
… kind

tests/registry_cache/run.sh installs the cache as the role renders it in a kind
cluster, with Docker Hub, ghcr.io and a private registry behind basic auth,
and runs the chart's runner pod template as the role renders a dind profile.
The step pulls from all three and builds through a docker-container builder
created as setup-buildx-action creates one, and the cache's access logs must
show every pull; with the cache scaled to nothing a second pod must still
pull and build from the registries directly.
The role README describes the variables, how runner pods use the cache,
which pull paths go through it and which bypass it, the written-out dind
wiring and what a credential on the cache exposes; the top-level README
summarises it and lists its integration test.
Runs tests/registry_cache/run.sh in kind on GitHub-hosted runners when the
ARC role, its filters or the test change, as capabilities.yml does for
capabilities.
…ered chart

Helm 4 prints its Pulled and Digest notice for an OCI chart on standard
output, which parses as a YAML mapping with no kind and stopped the
AutoscalingRunnerSet lookup with a KeyError.
dockerd looks up an image's referrers on every pull, and containerd sends
that lookup only to hosts with the referrers capability, so with a pull and
resolve mirror it went to the registry alone. For a registry that needs a
login the job has none, and the whole pull failed although the cache had
served every manifest and blob.
The private registry's image is pushed with crane from inside the cluster
rather than through a port-forward from the host, which a Docker daemon in
a VM cannot reach, the platform priority class the cache runs at is
created as the role creates it, Helm 4's pull notice is skipped when
reading the chart, and the docker-container builder also builds from the
private image.
@Mearman
Mearman marked this pull request as ready for review October 8, 2026 10:56
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ✅ Completed 2026-10-08T11:00:30.275798Z 700b2b5 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Mearman
Mearman merged commit 62c3cfc into main Oct 8, 2026
33 checks passed
@Mearman
Mearman deleted the feat/arc-registry-cache branch October 8, 2026 11:01
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 1.25.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an optional registry cache for the runner pods' Docker daemons

1 participant