feat(bootstrap): host every built image in the internal registry — GC-durable pulls

The disk-pressure drill's dead end: kubelet's image GC collects an unused image
and an air-gapped node has nothing to pull it from (ImagePullBackOff until an
operator re-imports). The registry the bundle already renders becomes that pull
source:

- Every image the installer builds is now a registry ref
  (registry.felis.svc:5000/felis/{felis,limbo,lobby,paper}:demo), imported into
  containerd under that exact name (first boot needs no registry round-trip)
  and mirrored into the registry after deploy_bundle (push_image_to_registry:
  push endpoint 127.0.0.1:5000, and only the path after the host matters to the
  registry — a push there lands where kubelet's mirrored pull looks). A ref
  outside the registry is warned about, not silently unmirrored.

- configure_registry_mirror writes /etc/rancher/k3s/registries.yaml mapping
  registry.felis.svc:5000 onto http://127.0.0.1:5000, the loopback hostPort the
  registry Deployment binds (node containerd cannot dial the Service VIP — live
  drill: "Empty reply"). k3s regenerates containerd config only at agent start,
  so a CONTENT change restarts k3s and an identical file (every re-run)
  restarts nothing.

- import_registry_image caches registry:2 into containerd so the registry
  Deployment can start on a box that cannot reach Docker Hub.

- Migration 0021 re-points the recommended whitelist seeds ('felis-lobby:demo',
  'felis-paper:demo') at the registry refs — a user server created from those
  rows must not strand when GC collects the bare tag. Only recommended rows
  still holding the old seed are touched; enabled is preserved; a pre-existing
  target row wins over a duplicate.

bootstrap_test.sh pins the mirror idempotence (identical content must NOT
restart k3s), the push-ref mapping (including the port-confusion refusal) and
the registry:2 precheck.
This commit is contained in:
Lemon-miaow committed 2026-09-23 19:02:58 +08:00
1 parent a9b275abbb
commit 13d64e0000
5 files changed
+303 -17

No files matched your search

+5 -5
View File
@@ -183,11 +183,11 @@ func (s *PGStore) ListImages(ctx context.Context) ([]Image, error) {
// Image it constructed (source=external) without re-reading the row, so a sticky
// source here would report a value the database does not hold.
//
// Note that the demote branch is unreachable for the ONLY recommended row Felis
// currently seeds: the caller validates with ValidateImageRef first, which refuses
// a bare local containerd tag, and 0018's felis-lobby:demo is exactly that. The
// branch is written for the host-qualified recommendations this list grows into,
// not for today's single seed.
// Note that this demote branch is REACHABLE for today's recommended rows: 0021
// re-pointed the seeds at host-qualified registry refs (registry.<ns>.svc:5000/…),
// which ValidateImageRef accepts — so an admin re-admitting one of those refs
// demotes the curated row, by design. It was dead only while the seeds were bare
// local containerd tags (0018's felis-lobby:demo), which the validation refuses.
func (s *PGStore) AddExternalImage(ctx context.Context, img Image) error {
const q = `INSERT INTO image_whitelist
(image_ref, source, added_by, enabled, added_at)
+3 -2
View File
@@ -126,8 +126,9 @@ type RegistryConfig struct {
// install there IS no such reach (the build egress policy allows only DNS,
// the internal registry and explicit package mirrors), so the operator must
// point these at whatever their box can actually pull — typically images
// imported into the node's containerd alongside the felis image. Empty keeps
// the default.
// mirrored into the in-cluster registry (docs/troubleshooting.md §8e); a
// bare node-containerd import does not survive an image GC, there is no pull
// source for it. Empty keeps the default.
KanikoImage string `toml:"kaniko_image"`
TrivyImage string `toml:"trivy_image"`
BuildCPULimit string `toml:"build_cpu_limit"`
@@ -0,0 +1,43 @@
-- Re-point the platform-seeded recommended images at the internal registry.
--
-- 0018/0019 seeded 'felis-lobby:demo' and 'felis-paper:demo' — the bare local
-- containerd tags the bootstrap of that day imported. The installer now builds
-- every image under registry.<ns>.svc:5000/felis/... and mirrors it into the
-- in-cluster registry, which is what lets kubelet re-pull an image the image GC
-- collected (drilled: disk pressure → game images collected →
-- ImagePullBackOff with no pull source). A bare local tag has no pull source at
-- all once its containerd copy is collected, so a user server created from a
-- recommended row would strand the same way. Re-point the seeds at the refs the
-- installer now builds — these MUST stay identical to deploy/bootstrap.sh's
-- FELIS_LOBBY_IMAGE / FELIS_PAPER_IMAGE defaults.
--
-- Only source='recommended' rows are touched, and only while the ref still IS
-- the old seed: a built/external row, or a recommended row an admin re-pointed
-- by hand, is theirs to keep. enabled is preserved either way — a disabled seed
-- stays disabled, just under its durable name.
--
-- Collision handling: image_ref is the primary key, so an UPDATE would abort if
-- the new ref already exists (e.g. an admin added it by hand). Keep whichever
-- row exists and drop the stale old one — never a duplicate, never an aborted
-- migration.
UPDATE image_whitelist
SET image_ref = 'registry.felis.svc:5000/felis/lobby:demo'
WHERE image_ref = 'felis-lobby:demo'
AND source = 'recommended'
AND NOT EXISTS (SELECT 1 FROM image_whitelist WHERE image_ref = 'registry.felis.svc:5000/felis/lobby:demo');
DELETE FROM image_whitelist
WHERE image_ref = 'felis-lobby:demo'
AND source = 'recommended'
AND EXISTS (SELECT 1 FROM image_whitelist WHERE image_ref = 'registry.felis.svc:5000/felis/lobby:demo');
UPDATE image_whitelist
SET image_ref = 'registry.felis.svc:5000/felis/paper:demo'
WHERE image_ref = 'felis-paper:demo'
AND source = 'recommended'
AND NOT EXISTS (SELECT 1 FROM image_whitelist WHERE image_ref = 'registry.felis.svc:5000/felis/paper:demo');
DELETE FROM image_whitelist
WHERE image_ref = 'felis-paper:demo'
AND source = 'recommended'
AND EXISTS (SELECT 1 FROM image_whitelist WHERE image_ref = 'registry.felis.svc:5000/felis/paper:demo');