Files
computer-playbook/docs/contributing/artefact/image/mirror.md
T
kevinveenbirkenbachandClaude Opus 5.5 088c8be14f fix(ci): pull workspace images through the GHCR mirror like the deploy jobs
The workspace jobs pulled every image anonymously from Docker Hub and
two of them failed on its rate limit in run 39059128678 (#990): the
fedora swarm drill on busybox:1.38 on all three nodes, the centos
compose drill while building tor-smtp_custom. The regular deploy jobs
pull through the GHCR mirror, but the mirror was tied to being a real
GitHub runner: Profile.image_mirror_enabled() returned
runs_on_github(), and the swarm routine generated mirrors.yml only when
cli.meta.runtime reported github, which is act inside make
swarm-zombie.

The mirror is now its own capability, INFINITO_GHCR_MIRROR (next to
INFINITO_GHCR_MIRROR_PREFIX), written as a run fact like
INFINITO_CACHE_STACK: true on a real GitHub runner,
false everywhere else, and a non-empty exported value wins for the run.
Profile.image_mirror_enabled() reads the declaration and keeps
runs_on_github() as the fallback, and 02_provision_inventory.sh gates
on the same value instead of the runtime name. make swarm-zombie
forwards it into act through ACT_ENV, since act ignores the host env.

call-test-workspace.yml declares INFINITO_GHCR_MIRROR=true and passes
INFINITO_GHCR_MIRROR_PREFIX into the workspace container. Nothing else
of the workspace changes: is_ci, runs_on_github, the cache stack and
the docker root stay local-like. The step that loads the rescue paths
now removes the .env it generated on the runner, because on debian
rows load.sh sourced it inside the container and the run inherited
INFINITO_RUNNING_ON_GITHUB=true.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 22:40:13 +02:00

103 lines
6.2 KiB
Markdown

# Image Mirroring 🪞
This document explains why image mirroring exists, how it works, and what each component does. For how images are declared in roles, see [origin.md](origin.md).
## Why Mirrors Exist 🎯
Upstream registries (Docker Hub, quay.io, mcr.microsoft.com, ghcr.io) impose **rate limits** and occasionally have **availability issues**. CI on GitHub-hosted runners also sees **IP-range- and geo-dependent failures**, plus transient `403`, `429`, and `5xx` responses when many jobs pull the same image directly from upstream. This is especially visible with Playwright images sourced from `mcr.microsoft.com`.
To solve this, CI mirrors all upstream images used in the project to GHCR (`ghcr.io`) under the project namespace before deploy tests run. Deploy jobs then pull from the GHCR mirror instead of the upstream registry, so a temporary upstream rate limit, outage, or geo-block does not fail an otherwise healthy change.
This also enables **fork PRs** to pull images without needing upstream credentials, because the mirror is public.
## Naming Convention 📛
Mirrored images follow this scheme:
```
ghcr.io/{namespace}/{repository}/mirror/{registry}/{name}:{version}
```
| Segment | Example |
|---|---|
| `namespace` | `kevinveenbirkenbach` |
| `repository` | `infinito-nexus-core` |
| `registry` | `docker.io`, `quay.io`, `mcr.microsoft.com`, `ghcr.io` |
| `name` | `nextcloud`, `keycloak/keycloak`, `prom/prometheus` |
| `version` | `latest`, `31.0.0` |
Example: `ghcr.io/kevinveenbirkenbach/infinito-nexus-core/mirror/docker.io/nextcloud:production-fpm-alpine`
## Components 🧩
### Image Discovery 🔍
`utils/docker/image/discovery.py` discovers the role images via `iter_role_images()`. It leaves out every role whose lifecycle is `eol` (`is_maintained()` in `utils/update/base.py`), so neither the mirror nor `mirrors.yml` covers those images. See [origin.md](origin.md) for the declaration format and supported registries.
### GHCRProvider 🐳
`cli/contributing/mirror/providers.py` contains the `GHCRProvider` class, which computes the destination image name via `image_base(img)` and provides `add_args` / `from_args` so all CLI tools share a single argument definition (SPOT).
### Resolver 🗺️
`cli/contributing/mirror/resolver/__main__.py` reads all role images and outputs a `mirrors.yml` file under a single `applications.<role>.services.<service>` map:
```yaml
applications:
<role>:
services:
<service>:
image: ghcr.io/{namespace}/{repository}/mirror/{registry}/{name}
version: <tag>
```
This file is consumed by the inventory creator to substitute mirror URLs into host variables.
### Sync 🔄
`cli/contributing/mirror/sync/__main__.py` copies each image from the upstream source to the GHCR mirror destination using `skopeo copy`. Supports `--only-missing` to skip already-mirrored images and `--images-per-hour` for throttling.
### Wait Script ⏳
`scripts/meta/wait/mirrors.sh` waits for fork PRs until all required mirror images are available in GHCR before letting the deploy tests proceed. This is necessary because fork PRs cannot push images themselves; the `pull_request_target` event handles that.
## CI Integration 🔄
The mirror workflow runs as stage 8 of the CI pipeline. See [ci.md](../git/pipeline.md). It runs in parallel with the DNS tests and MUST complete before deploy tests start.
### CI Flow 📋
1. Image discovery scans role declarations in `meta/services.yml`.
2. [call-images-mirror-missing.yml](../../../../.github/workflows/call-images-mirror-missing.yml) copies only missing upstream refs into GHCR via `cli.contributing.mirror.sync --only-missing`. Optional Docker Hub credentials reduce source-side rate limits during that sync.
3. Fork PRs cannot publish packages themselves, so their untrusted `pull_request` runs wait in `scripts/meta/wait/mirrors.sh` until a trusted producer run has published the required refs.
4. Inventory generation writes the resulting mirror refs from `mirrors.yml` back into host vars.
5. Deploy and test jobs resolve images from `ghcr.io/{namespace}/{repository}/mirror/...` instead of pulling directly from Docker Hub, MCR, or other upstream registries.
`INFINITO_GHCR_MIRROR` decides whether a run uses the mirror. The generated `.env` sets it to `true` on a real GitHub runner and to `false` everywhere else, act included. A non-empty exported value overrides that for one run. [call-test-workspace.yml](../../../../.github/workflows/call-test-workspace.yml) exports `true`, so the workspace suite runs like a local setup and still pulls from GHCR, in its compose drills and, through `make swarm-zombie`, in its swarm drills.
The mirrors file is generated into the inventory directory.
The inventory creator applies mirror image overrides to host variables via `cli/administration/inventory/provision/mirror_overrides.py`, which reads `mirrors.yml.applications` and writes `applications.{role}.services.{service}.image` (and `.version`) into host vars subject to the per-service `mirror_policy`.
Role templates read image refs via `lookup('config', '<role>', 'services.<service>.image')` / `.version`, which resolves against the merged `applications.*` map.
The mirror override applied above is what consumers see.
### Mirror Policy 📋
Each service in host vars MAY carry a `mirror_policy` field that controls how the override is applied:
| Policy | Behavior |
|---|---|
| `if_missing` (default) | Fill `image`/`version` only if they are blank or absent |
| `force` | Always overwrite `image`/`version` from the mirror |
| `skip` | Never touch this service |
## Adding a New Mirrored Image 🆕
No manual registration is needed. Images declared in a role are automatically discovered and included in the next mirror run. See [origin.md](origin.md) for the correct declaration format.
For CI-critical images, you SHOULD pin an exact upstream tag instead of using mutable tags such as `latest`. This is especially important for Playwright images mirrored from `mcr.microsoft.com`, where exact tags avoid CDN propagation races and make flaky upstream releases easier to diagnose.
## Cleanup 🗑️
To delete stale private GHCR mirror packages (e.g. packages pushed before `GITHUB_TOKEN` authentication was adopted), see [cleanup.md](../../tools/ghcr/cleanup.md).