Files
computer-playbook/docs/contributing/environment/cache.md
T
kevinveenbirkenbachandClaude Opus 5.5 ca6faac318 fix(ci): share the vulnerability databases with act runs and local deploys
Under act the deploy job pinned an engine but skipped the cache restores, so
every local swarm-* run stopped on a missing Grype database. The swarm
matrix also ran inside the act job container, where the shared host
directory did not exist, and the workspace job would have kept a copy of
about 5 GB in every worktree.

- scripts/tests/deploy/act/workflow.sh requires
  INFINITO_VULNERABILITY_DB_HOST_PATH, creates it on the host and binds it
  into the job container at the same path, because that container drives
  the host daemon, which resolves every stack and swarm node bind on the
  host.
- call-test-deploy.yml pins engine and database path in one step that runs
  only outside act; under act the deploy draws an engine and refreshes the
  shared directory. call-test-workspace.yml takes the shared path under act.
- The refresh lock moves into the shared directory, so host deploys and act
  runs serialize on one file, and a root refresh hands its files to the
  directory owner. hand_over skips paths a mocked download never created,
  which failed four TestRefresh cases when the gate ran as root.
- A missing INFINITO_VULNERABILITY_DB_HOST_PATH stops compose up with a
  "run 'make dotenv'" hint instead of a KeyError.
- Rationale paragraphs in the database and compose docstrings are dropped.

Part of #1324

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 02:44:59 +01:00

11 KiB
Raw Blame History

Local Caches 📦

Three local cache services accelerate re-deploys, reduce upstream traffic, and harden the development workflow against transient upstream outages. They share one compose profile (cache) and one override file (compose/cache.override.yml) and are gated together by profile.py: active on developer machines, inactive on CI runners.

Services 🧩

Registry Cache 🐳

registry-cache runs rpardini/docker-registry-proxy and transparently MITMs every Docker image pull from the inner dockerd, regardless of upstream registry (docker.io, ghcr.io, mcr.microsoft.com, …). The runner trusts the proxy CA via registry-ca.sh.

Pulls only. Pushes are not intercepted.

Package Cache 📚

package-cache runs sonatype/nexus3 and exposes pull-through proxies for package-manager downloads:

Format Repo names Upstream
apt apt-debian, apt-debian-security, apt-ubuntu, apt-ubuntu-security deb.debian.org; both Ubuntu repos take the first entry of INFINITO_APT_UBUNTU_MIRRORS
apt (mirrors) apt-debian-mirror, apt-debian-security-mirror, apt-ubuntu-mirror, apt-ubuntu-security-mirror ftp.debian.org, security.debian.org; both Ubuntu repos take the second entry of INFINITO_APT_UBUNTU_MIRRORS
pypi pypi-proxy pypi.org (incl. files.pythonhosted.org)
npm npm-proxy registry.npmjs.org
rubygems gem-proxy rubygems.org
go go-proxy proxy.golang.org
helm helm-bitnami charts.bitnami.com/bitnami
yum yum-rocky, yum-fedora download.rockylinux.org, download.fedoraproject.org
raw raw-githubusercontent, raw-codeload-github, raw-packagist, raw-alpine raw.githubusercontent.com, codeload.github.com, repo.packagist.org, dl-cdn.alpinelinux.org

A proxy repo that holds a cached copy serves it when its remote is unreachable, regardless of metadataMaxAge (measured). autoBlock is therefore off: while a remote is auto-blocked Nexus answers 404 Remote Auto Blocked instead of falling back to that cached copy. The mirror repos below only come into play when nothing is cached either.

Every apt suite exists twice, once per upstream mirror. Nexus has no group repository type for apt (only maven, raw, docker, yum, npm, pypi, rubygems, go and friends), so the failover lives in the frontend: a request that the primary repo answers with 502/503/504 is re-run against the -mirror repo, which proxies a different host. 404 deliberately does not retry, because a missing file is missing on both mirrors and apt probes for missing files often. The frontend waits at most 8 s for a primary before it re-runs the request against the mirror; the -mirror locations keep the global 300 s.

Bootstrap is idempotent and runs from package.sh once the stack is healthy.

Package Cache Frontend 🔐

package-cache-frontend runs nginx:alpine and reverse-proxies upstream package-manager hostnames onto the matching Nexus repo path. Combined with extra_hosts DNS-hijack on consumers, package managers can hit their real upstream URL and still flow through the cache.

Two listener layers:

  • HTTPS (port 443): per-hostname server certs signed by a dedicated CA. Used by the infinito runner (Ansible-driven pip install, gem install, composer install, curl https://…). The runner trusts the CA via package-frontend-ca.sh.
  • HTTP (port 80): plain mirrors for deb.debian.org, archive.ubuntu.com, security.ubuntu.com, dl-cdn.alpinelinux.org. Used by inner-dockerd Dockerfile builds via build.extra_hosts DNS-hijack. No CA-trust required in the build container.

Cert generation runs in a throw-away alpine container driven by package-frontend-certs.sh before the frontend starts.

Activation 🎚️

The cache decision is exposed via Profile.cache_stack_enabled() in profile.py. When active:

  • compose/cache.override.yml is layered on top of the base compose.yml by compose.py and down.py via common.pycompose_file_args.
  • The cache services are added.
  • The runner's infinito service receives:
    • bind-mounts for the registry-cache CA, the package-cache client snippets (pip.conf, npmrc, apt/${INFINITO_DISTRO}.list), and the frontend CA file
    • extra_hosts entries DNS-hijacking the HTTPS upstream hostnames to the frontend's static IP
    • INFINITO_CACHE_PACKAGE_FRONTEND_IP env var for the inner compose wrapper
  • Cert generation, Nexus repo bootstrap, and runner trust-store install run from compose.py.

When inactive, the override is omitted: cache services do not exist, the runner has no cache mounts or DNS-hijack, package managers go direct to upstream.

CI signals (GITHUB_ACTIONS=true, INFINITO_RUNNING_ON_GITHUB=true, CI=true) deactivate. Fresh runner disks per CI job give no cross-run amortization.

Coverage Matrix 📋

Traffic Mechanism Cached
Image pulls (any registry, inner dockerd) registry-cache MITM via HTTP_PROXY env on dockerd ✓
apt-get install (Ansible task in runner) apt/${INFINITO_DISTRO}.list URL-rewrite to package-cache:8081 ✓
pip install (Ansible task in runner) pip.conf URL-rewrite ✓
npm install (Ansible task in runner) .npmrc URL-rewrite ✓
gem, composer, go, curl https://… (Ansible task in runner) DNS-hijack + frontend HTTPS + runner CA-trust ✓
RUN apt-get install (inner Dockerfile build, Debian/Ubuntu) build.extra_hosts DNS-hijack + frontend HTTP ✓
RUN apk add (inner Dockerfile build, Alpine ≤3.17 HTTP) build.extra_hosts DNS-hijack + frontend HTTP ✓
RUN pip/npm/gem/composer/go install (inner Dockerfile build) none today ✗
RUN apk add (Alpine ≥3.18 HTTPS) none today ✗
App container runtime traffic (HTTPS to external API) none today ✗

The HTTPS-only inner-build gap requires per-image CA-trust bootstrap, which is out of scope for the current minimal-invasive design.

Compose Wrapper Auto-Detection 🪄

The runner's compose wrapper at roles/sys-svc-compose/files/python/compose.py auto-detects compose files when invoked from per-app directories under /opt/compose/<app>/:

File When
compose.yml always
compose.override.yml when present (role provides it)
compose.ca.override.yml when present (TLS self-signed CA-inject runs)
compose.cache.override.yml generated on the fly when INFINITO_CACHE_PACKAGE_FRONTEND_IP is set; emits build.extra_hosts for every service that has a build: key

pull.py delegates to the same wrapper so pull and build --pull operations see the identical -f set.

Environment Variables 🌳

The host-side env-vars (INFINITO_CACHE_REGISTRY_*, INFINITO_CACHE_PACKAGE_*) are declared in default.env and the dynamic ones (cache sizes from df//proc/meminfo, sha256 admin password) are computed by utils/env/builder.py. make dotenv writes them into .env, which Compose auto-loads and which BASH_ENV makes available to every Makefile recipe through scripts/meta/env/load.sh. See variables.md.

Per-variable defaults and purposes are in compose.yml.md under the cache section.

INFINITO_CACHE_PACKAGE_MAX_AGE_MIN (default 8640 = 6 days) controls how long every Nexus proxy repo holds an upstream response before revalidating. The bootstrap helper applies it as contentMaxAge, metadataMaxAge, and negativeCache.timeToLive. The default is intentionally one day below the 7-day Valid-Until window Debian and Ubuntu generate in their apt Release files, so Nexus always refreshes the manifest before apt-get update would see it expired. Raise it for slow-moving upstreams when staleness is preferable to upstream load; lower it further for fast-moving feeds (apt-debian-security, branch-pinned tarballs) when staleness becomes visible.

Operations 🛠️

Action Command
Start the stack with caches make compose-up
Stop the stack make compose-down
Wipe local cache state make clean-cache
Manually re-bootstrap Nexus repos bash scripts/docker/cache/package.sh (after make dotenv or sourcing scripts/meta/env/load.sh)
Manually regenerate frontend certs bash scripts/docker/cache/package-frontend/certs.sh
Reload nginx in the frontend docker exec infinito-package-cache-frontend nginx -s reload
Inspect cache hits docker logs -f infinito-package-cache and docker logs -f infinito-package-cache-frontend

Cache state persists under /var/cache/infinito/core/cache/. Paths are configurable via the env scripts above.

Vulnerability Databases 🔬

Every development stack and every swarm lab node mounts INFINITO_VULNERABILITY_DB_HOST_PATH read-only at INFINITO_VULNERABILITY_DB_DIR. A local deploy draws one engine and refreshes that engine's and Grype's database there once a day, under a lock file inside that directory, before the deploy starts; the stacks never download one. An act run binds the directory into its job container at the same path, and a refresh as root hands its files to the directory's owner. A CI deploy job points the variable at the databases it restored from the Actions cache instead. See sys-ctl-hlth-runtime.

Adding a New Upstream ➕

When a new package manager or upstream needs caching:

  1. Register a Nexus proxy repo in package.sh.
  2. If the upstream uses HTTPS, add it to HOSTNAMES in package-frontend-certs.sh so a leaf cert is issued.
  3. Add a server-block in upstreams.conf that reverse-proxies onto the new Nexus repo path (rewrite if upstream URL prefix differs from the Nexus repo path).
  4. Add an extra_hosts entry on the infinito service in compose/cache.override.yml for runner-side traffic.
  5. If the upstream is HTTP-only and inner-dockerd builds need it, also add it to _CACHE_HTTP_HOSTNAMES in compose.py so the per-app compose.cache.override.yml includes it in build.extra_hosts.

Background 📚