feat(images): mark platform-curated images and seed the lobby

The create-server form has no way to tell a user which of the whitelisted
images is a sensible starting point. Add 'recommended' as a third
image_whitelist.source alongside 'built' and 'external', and seed it with
the one image that has earned it.

The marker is presentation only. ImageAdmitted still turns solely on
enabled, so a recommended row is admitted by exactly the rule that governs
every other row and carries no extra privilege; a test pins both halves,
because the failure modes are silent and opposite — make admission
source-aware and the curated images vanish from the form, or let curation
bypass the disable switch and an admin who pulled an image finds it still
creatable.

Only one image is seeded, and the restraint is the point. Velocity runs
proxy-wide modern forwarding, so a backend that cannot verify the signed
handshake rejects every login the proxy sends it. The operator injects
FELIS_FORWARDING_SECRET into every backend but cannot make an image consume
it. An arbitrary public Minecraft image therefore passes admission, builds,
schedules, reports Ready — and then refuses every join, with nothing in the
server's status explaining why. Exactly two images read that variable,
deploy/limbo and deploy/lobby; limbo is the login gate and is nonsense as a
base for a user's server, which leaves lobby. The list grows when Felis
ships another forwarding-aware image, not before.

AdmitBuiltImage now preserves a 'recommended' source through its ON CONFLICT
path. Rebuilding a curated tag is the expected way to patch it, and that
rebuild arrives through this exact path, so a blind SET source = 'built'
would demote the curation on the first rebuild with nothing in the request
saying so. AddExternalImage deliberately does not preserve it: an admin
POSTing the ref is an explicit, named re-admission, and the 201 body reports
the Image it constructed without re-reading the row, so a sticky source
there would report a value the database does not hold.

The migration is idempotent via ON CONFLICT DO NOTHING, so an admin who
disabled or re-pointed the row does not have that decision undone on the
next apply.
This commit is contained in:
flyemoji committed 2026-07-20 14:31:14 +09:00
1 parent d26acc20ae
commit f36d5b87f6
4 files changed
+139 -2

No files matched your search

@@ -0,0 +1,62 @@
-- Recommended images: platform-curated entries the create-server form can offer
-- ahead of the rest of the whitelist. image_whitelist.source is plain text, not
-- an enum, so this needs no schema change — 'recommended' simply joins 'built'
-- and 'external' as a third provenance (see internal/build.SourceRecommended).
-- The marker is presentation only: admission still turns solely on enabled, so a
-- recommended row is gated by exactly the same rule as every other row.
--
-- WHY THIS SEEDS ONE IMAGE AND NOT SEVERAL. The obvious version of this feature
-- — recommend a handful of popular Minecraft images from Docker Hub — ships a
-- trap. Velocity runs modern forwarding, which is a proxy-WIDE setting: with it
-- on, a backend that cannot verify the signed handshake rejects every login the
-- proxy forwards. The operator injects FELIS_FORWARDING_SECRET into every
-- backend pod (operator.buildEnv) but cannot make an image consume it, and an
-- image built from an operator-typed Dockerfile does not. So an arbitrary public
-- image passes admission, builds, schedules, and reports Ready — and then is
-- UNJOINABLE, failing at the last step with nothing in the server's status
-- explaining why. Recommending that is worse than recommending nothing.
--
-- Grep for FELIS_FORWARDING_SECRET: exactly two images read it in their
-- entrypoints, deploy/limbo and deploy/lobby, and both refuse to start without
-- it. Those two are the entire joinable set. Of them:
--
-- * limbo is the login gate — a system server pinned to the reserved name
-- "login" plus the setup-owned system-role label, and the only workload that
-- receives FELIS_SERVICE_TOKEN. It has no world and no gameplay; it exists to
-- hold a player at the bind screen. Recommending it as a base for a user's
-- own server would be nonsense.
-- * lobby is Paper plus the felis-paper /menu plugin: a real, joinable,
-- playable server and a sound starting point for a user's own.
--
-- That leaves exactly one defensible recommendation. Seeding a second entry
-- would mean padding the list with an image that cannot carry a player, so this
-- migration ships the honest set of one. The list grows when Felis ships another
-- forwarding-aware image, not before.
--
-- REF CAVEAT: felis-lobby:demo is the bootstrap default (FELIS_LOBBY_IMAGE in
-- deploy/bootstrap.sh and deploy/demo-up.sh), built locally and imported into
-- k3s containerd. An install that overrode that variable runs a different ref,
-- and this row will point at an image its cluster does not have. That failure is
-- deliberately the loud kind — the pod ImagePullBackOffs immediately and is
-- visible in server status, rather than starting and silently refusing joins —
-- and an admin clears it with DELETE /images?ref=felis-lobby:demo, which is
-- unvalidated and always works. Re-adding the real ref is NOT symmetric: POST
-- /images runs ValidateImageRef, which requires a host-qualified reference
-- (splitRegistryHost wants a first segment carrying '.' or ':'), so it accepts
-- registry.example:5000/lobby:v2 but REFUSES a bare local containerd tag like
-- my-lobby:v2 — the very shape bootstrap builds. An override that lives only in
-- the node's image store therefore has no API path back in and must be seeded the
-- same way this row was, in SQL. That asymmetry is why this seed is SQL and not a
-- POST. The seed cannot do better on its own: the correct value is operator
-- configuration ([velocity] lobby_image), which is not readable from SQL.
--
-- added_by records provenance rather than a person: no human admitted this row,
-- the platform did, and the audit trail should say so instead of attributing it
-- to whoever happened to run the migration.
--
-- Idempotent by ON CONFLICT DO NOTHING: migrations may re-run, and an admin who
-- deliberately disabled or re-pointed this row must not have that decision
-- silently undone on the next apply.
INSERT INTO image_whitelist (image_ref, source, added_by, enabled)
VALUES ('felis-lobby:demo', 'recommended', 'felis-platform', true)
ON CONFLICT (image_ref) DO NOTHING;