feat(deploy): login-limbo and lobby images with game-port pinning

deploy/limbo assembles LOOHP/Limbo from its loose CI artifacts plus the felis-limbo plugin (and the shared link core), with an entrypoint that pins server-port to the operator's GamePort (25565) on every start. deploy/lobby carries the Paper + felis-paper hub image. .dockerignore re-includes plugins/limbo and plugins/shared so the plugin image build sees them.
This commit is contained in:
flyemoji committed 2026-07-02 19:38:38 +09:00
1 parent 241fe21f8a
commit c7315e44b6
6 files changed
+361 -1

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
# Felis login-limbo image: LOOHP/Limbo + the felis-limbo readiness plugin.
#
# CODE-ONLY in this repo — it is not built by the Go CI. It packages the always-on
# "login" auth gate the setup provisioner points [velocity] login_image at.
#
# LOOHP/Limbo has no official image and no release zip. Its CI
# (ci.loohpjames.com/job/Limbo) publishes two LOOSE artifacts per build:
# - target/Limbo-<ver>.jar (the server jar; Main-Class com.loohp.limbo.Limbo)
# - spawn.schem (the default spawn schematic)
# There is no bundled server.properties — Limbo writes a default on first run.
# So the runtime is assembled from those two URLs (not a zip) via --build-arg:
#
# docker build -f deploy/limbo/Dockerfile \
# --build-arg LIMBO_JAR_URL=https://ci.loohpjames.com/job/Limbo/<n>/artifact/target/Limbo-<ver>.jar \
# --build-arg LIMBO_SCHEM_URL=https://ci.loohpjames.com/job/Limbo/<n>/artifact/spawn.schem \
# --build-arg LIMBO_VERSION=<maven-api-version> \
# -t felis-limbo:demo .
#
# Note LIMBO_VERSION (the maven API version the plugin compiles against, e.g.
# 2026.0.2-ALPHA) differs from the jar's CI build-qualified filename (e.g.
# Limbo-2026.0.2-ALPHA-26.2.jar): the CI qualifier is not published to the maven
# repo. Then import it the same way as the control-plane image (k3s ctr import)
# and set [velocity] login_image = "felis-limbo:demo" in felis.toml before setup.
#
# Two contracts the operator depends on:
# 1. The game server listens on 25565 (the CRD's GamePort).
# 2. The felis-limbo readiness endpoint listens on 8080 (StartupSpec.HealthHTTPPort,
# overridable via FELIS_HEALTH_PORT) and returns 200 only after the first tick.
# ---- build the felis-limbo plugin jar ----
# gradle:*-jdk21 — an official Gradle image on JDK 21. JDK 21 is required because
# current LOOHP/Limbo releases ship Java 21 API classes (class-file major 65); a
# JDK 17 fails to read them with "wrong version 65.0, should be 61.0". The image
# also provides the `gradle` binary (this tree vendors no Gradle wrapper).
# build.gradle still targets release 17 bytecode so the plugin loads on Java 17+.
FROM gradle:8.14-jdk21 AS plugin
WORKDIR /src
# Copy what the limbo module needs: its own tree plus the shared link core it
# srcDir-includes (../shared/src/main/java → /src/plugins/shared/src/main/java), so
# the account-link client + config loader compile straight into the jar.
COPY plugins/limbo/ ./plugins/limbo/
COPY plugins/shared/ ./plugins/shared/
ARG LIMBO_VERSION=+
RUN cd plugins/limbo \
&& (test -x ./gradlew && ./gradlew --no-daemon -PlimboVersion="$LIMBO_VERSION" build \
|| gradle --no-daemon -PlimboVersion="$LIMBO_VERSION" build) \
&& cp build/libs/*.jar /felis-limbo.jar
# ---- assemble the runtime ----
# 21-jre: the Limbo jar is Java 21 bytecode (class-file major 65), so a Java 17
# JRE cannot run it (UnsupportedClassVersionError). A 21 JRE also runs the
# plugin's release-17 bytecode fine.
FROM eclipse-temurin:21-jre
ARG LIMBO_JAR_URL
ARG LIMBO_SCHEM_URL
WORKDIR /limbo
# Pull the two loose LOOHP/Limbo CI artifacts: the server jar (required, saved as
# Limbo.jar) and the default spawn schematic (optional). Fail loudly if the jar
# URL was not supplied.
RUN set -eu; \
if [ -z "${LIMBO_JAR_URL:-}" ]; then \
echo "ERROR: --build-arg LIMBO_JAR_URL=<Limbo server jar> is required" >&2; exit 1; \
fi; \
apt-get update && apt-get install -y --no-install-recommends curl ca-certificates; \
curl -fSL "$LIMBO_JAR_URL" -o /limbo/Limbo.jar; \
if [ -n "${LIMBO_SCHEM_URL:-}" ]; then \
curl -fSL "$LIMBO_SCHEM_URL" -o /limbo/spawn.schem; \
fi; \
apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*; \
mkdir -p /limbo/plugins
# Drop the login+readiness plugin in beside Limbo.jar.
COPY --from=plugin /felis-limbo.jar /limbo/plugins/felis-limbo.jar
# The entrypoint pins the game port to the operator's GamePort before launching Limbo.
COPY deploy/limbo/entrypoint.sh /usr/local/bin/felis-entrypoint.sh
ENV FELIS_HEALTH_PORT=8080
# FELIS_GAME_PORT is the port the entrypoint pins Limbo to; it MUST equal the operator's
# GamePort (internal/operator/builders.go). Default 25565 — override only in lockstep
# with the operator.
ENV FELIS_GAME_PORT=25565
EXPOSE 25565 8080
# felis-entrypoint.sh pins server-port then execs `java -jar Limbo.jar --nogui` (headless:
# the pod has no console). Limbo writes the rest of server.properties on first run and
# loads ./spawn.schem as the spawn world. Invoked via `sh` so no +x bit is needed from the
# (Windows) build host.
ENTRYPOINT ["/bin/sh", "/usr/local/bin/felis-entrypoint.sh"]
+142
View File
@@ -0,0 +1,142 @@
# Login-limbo image (LOOHP/Limbo + felis-limbo)
The always-on **login** auth gate. `felis setup` provisions it as a system
service (`DesiredState=Running`, reaper-exempt) when `[velocity] login_image` is
set in `felis.toml`. Every fresh connection lands here first; it is the only safe
fallback (a stopped/starting backend routes here, never past authentication).
> **Code-only.** This image is not built by the Go CI. It compiles the
> `plugins/limbo` plugin (plus the shared `plugins/shared` link core it
> srcDir-includes) and bundles it with a LOOHP/Limbo release.
## What the plugin does
`felis-limbo` does two jobs — readiness and the in-game login flow.
### Readiness
LOOHP/Limbo has no RCON, so Felis cannot use its usual RCON readiness probe
(spec §5). A bare TCP check would report "ready" the instant the socket binds.
The plugin instead serves an HTTP readiness endpoint that flips to `200` only
after the first server tick — i.e. once the limbo has genuinely started. The
`login` MinecraftServer sets `spec.startup.healthHTTPPort: 8080`, so the pod's
HTTP readinessProbe (and, with RCON disabled, the operator's Ready gate) follows
that true signal.
- Endpoint: `GET /healthz` on `:8080` (override with `FELIS_HEALTH_PORT`).
- `503 starting` before the first tick, `200 ok` after.
- Fail-closed: if the endpoint cannot bind, readiness never turns green and the
operator keeps the gate in `Starting` — it never advertises an unstarted gate.
### In-game login (spec §B3)
A player reaching the limbo has been UUID-verified upstream (Velocity online-mode)
but is not yet linked to a web account. On join, off the tick thread, the plugin:
1. checks the username-collision **blacklist** and disconnects a barred squatter
UUID (the genuine Mojang player — different UUID — passes);
2. mints a one-time **Bind Code** for the verified UUID via the felis-api internal
face;
3. opens a **book** with a clickable link to `console.<root_domain>` plus a chat
line carrying the code, and tells the player to finish in their **system**
browser — never the WeChat/QQ in-app browser, where passkey/WebAuthn does not
work (the web entry additionally guards this; see `internal/panel`);
4. **polls** `link/status/{uuid}` until the player redeems the code on the web
console, then **transfers** them to the lobby via a BungeeCord `Connect` plugin
message on `bungeecord:main`;
5. **disconnects (fail-closed)** on blacklist, on a mint/transport failure, or when
the login window elapses — "rather refuse than admit unauthenticated".
Configuration (deployment inputs, never compiled in; env wins over a
`felis-link.properties` template written in the plugin data dir on first run):
| Env | Meaning | Default |
| --- | ------- | ------- |
| `FELIS_API_BASE_URL` | felis-api **internal** face base URL | *(required for login)* |
| `FELIS_SERVICE_TOKEN` | internal service token (secret) | *(required for login)* |
| `FELIS_ROOT_DOMAIN` | deployment zone, builds `https://console.<zone>` | *(required for login)* |
| `FELIS_LOBBY_SERVER` | Velocity server name to transfer to | `lobby` |
| `FELIS_LOGIN_TIMEOUT_SECONDS` | login window (clamped 30–3600) | `300` |
| `FELIS_HEALTH_PORT` | readiness port | `8080` |
If the API config **or** the root domain is absent the login flow stays **OFF** and
the plugin runs readiness-only (the same "load un-crippled" fail-safe the other
Felis plugins use), so a bare image still boots — production must supply the config
for the gate to authenticate. Transfer requires Velocity to accept the BungeeCord
plugin-message channel (`bungee-plugin-message-channel` on the proxy).
## Build
LOOHP/Limbo has no official image and no release zip. Its CI
(`ci.loohpjames.com/job/Limbo`) publishes two **loose** artifacts per build —
`target/Limbo-<ver>.jar` and `spawn.schem` — so the image is assembled from those
two URLs (there is no bundled `server.properties`; Limbo writes a default on first
run):
```
docker build -f deploy/limbo/Dockerfile \
--build-arg LIMBO_JAR_URL=https://ci.loohpjames.com/job/Limbo/<n>/artifact/target/Limbo-<ver>.jar \
--build-arg LIMBO_SCHEM_URL=https://ci.loohpjames.com/job/Limbo/<n>/artifact/spawn.schem \
--build-arg LIMBO_VERSION=<maven-api-version> \
-t felis-limbo:demo .
```
- `LIMBO_JAR_URL` (required) — the server jar; it is saved as `Limbo.jar`.
- `LIMBO_SCHEM_URL` (optional) — the default spawn schematic, saved as
`spawn.schem` and loaded as the spawn world.
- `LIMBO_VERSION` — the Limbo **maven** API version the plugin compiles against
(Gradle `-PlimboVersion`). This differs from the jar's CI build-qualified
filename: e.g. the jar `Limbo-2026.0.2-ALPHA-26.2.jar` corresponds to maven
version `2026.0.2-ALPHA` (the `-26.2` CI qualifier is not published to the
maven repo).
Import into k3s and point config at it:
```
docker save felis-limbo:demo | sudo k3s ctr images import -
# felis.toml → [velocity] login_image = "felis-limbo:demo"
sudo felis setup
```
## Ports (handled for you)
The entrypoint (`deploy/limbo/entrypoint.sh`) pins Limbo's `server-port` to
`FELIS_GAME_PORT` (default **25565**, the operator's `GamePort`) on every start —
LOOHP/Limbo would otherwise default to `30000`, unreachable through the Velocity
`NetworkPolicy` / Service / probe the operator drives off that one const. It is
idempotent, so a persisted world volume keeps all its other `server.properties`
settings. Do **not** override `FELIS_GAME_PORT` except in lockstep with the operator.
## Configure (deployer's responsibility)
One setting this image does **not** guess (it keeps the release's own default):
- **Player forwarding** — align Limbo's forwarding with the off-cluster Velocity
proxy so authenticated players hand off cleanly, and enable the BungeeCord
plugin-message channel on the proxy so the login gate's `Connect` transfer to the
lobby lands.
The login flow's own inputs (`FELIS_API_BASE_URL`, `FELIS_ROOT_DOMAIN`,
`FELIS_LOBBY_SERVER`, `FELIS_SERVICE_TOKEN`; see
**[In-game login](#in-game-login-specb3)** above) are wired in for you — you do not
set them by hand:
- The three **non-secret** vars are baked into the `login` MinecraftServer's
`spec.env` by `felis setup` (`cmd/felis` derives the internal API URL from the
control namespace — the platform default `felis`; a renamed control namespace must
be reflected by hand — and the root domain from `felis.toml`).
- `FELIS_SERVICE_TOKEN` is a **secret**, so it is never written into the CRD. `felis
setup` replicates the `felis-service-token` Secret from the control namespace into
the minecraft namespace, and the operator injects it into the `login` pod (only)
via a `secretKeyRef`, keyed off the reserved `login` name. Until the token is
present the plugin fail-safes to readiness-only, so the gate is never broken — it
simply does not authenticate yet.
- **NetworkPolicy:** none is required today — neither the minecraft-namespace egress
nor the control-namespace ingress is policy-locked, so the login pod's call to the
API internal port is already reachable. If a future deployment adds a minecraft
egress lock or a control-namespace ingress fence, it must also open the
login-pod → felis-api internal-port (8081) path.
The Velocity default-landing and waiting-park wiring is printed by `felis setup`
and enforces the invariant: fresh connections hit `login` first; nothing falls
back to the lobby.
+31
View File
@@ -0,0 +1,31 @@
#!/bin/sh
# Felis login-limbo entrypoint.
#
# Pin Limbo's game port to the pod-facing port the operator contract uses. LOOHP/Limbo
# defaults server-port to 30000, but the Felis operator drives everything — the Service
# Port/TargetPort, the TCP/HTTP readiness probe, the container port and the Velocity
# NetworkPolicy — off a single GamePort const (25565). A backend that bound 30000 would
# be unreachable through that fence. Limbo writes a full server.properties on first run
# and merges any partial we leave in place, so seeding/patching just server-port here is
# enough; the spawn schematic still loads from ./spawn.schem.
#
# Idempotent by design: it runs on every start and rewrites only the server-port line,
# so a persisted world volume that already carries a server.properties keeps all its
# other settings.
set -eu
PORT="${FELIS_GAME_PORT:-25565}"
PROPS="server.properties"
if [ -f "$PROPS" ]; then
if grep -q '^server-port=' "$PROPS"; then
sed -i "s/^server-port=.*/server-port=${PORT}/" "$PROPS"
else
printf 'server-port=%s\n' "$PORT" >> "$PROPS"
fi
else
printf 'server-port=%s\n' "$PORT" > "$PROPS"
fi
echo "felis-limbo: pinned server-port=${PORT} (operator GamePort)"
exec java -jar Limbo.jar --nogui "$@"
+47
View File
@@ -0,0 +1,47 @@
# Felis lobby image: Paper + the felis-paper /menu plugin.
#
# CODE-ONLY in this repo — not built by the Go CI. It packages the always-on
# "lobby" hub the setup provisioner points [velocity] lobby_image at. The lobby is
# the POST-auth /menu hub: it is reached only when the login gate transfers an
# authenticated player onward, and it must never be a fallback target.
#
# Build:
# docker build -f deploy/lobby/Dockerfile \
# --build-arg PAPER_JAR_URL=https://<mirror>/paper-1.21.x-<build>.jar \
# -t felis-lobby:demo .
# docker save felis-lobby:demo | sudo k3s ctr images import -
# # felis.toml → [velocity] lobby_image = "felis-lobby:demo"
#
# Contract: the game server listens on 25565 (the CRD GamePort). The lobby speaks
# only the felis:control plugin-message channel (spec §12) — it holds no felis-api
# token.
# ---- build the felis-paper plugin jar (Paper API is Java 21) ----
FROM eclipse-temurin:21-jdk AS plugin
WORKDIR /src
COPY plugins/paper/ ./plugins/paper/
COPY plugins/shared/ ./plugins/shared/
RUN cd plugins/paper \
&& (test -x ./gradlew && ./gradlew --no-daemon build \
|| gradle --no-daemon build) \
&& cp build/libs/*.jar /felis-paper.jar
# ---- assemble the runtime ----
FROM eclipse-temurin:21-jre
ARG PAPER_JAR_URL
WORKDIR /paper
RUN set -eu; \
if [ -z "${PAPER_JAR_URL:-}" ]; then \
echo "ERROR: --build-arg PAPER_JAR_URL=<paper jar> is required" >&2; exit 1; \
fi; \
apt-get update && apt-get install -y --no-install-recommends curl ca-certificates; \
curl -fSL "$PAPER_JAR_URL" -o /paper/paper.jar; \
apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*; \
mkdir -p /paper/plugins; \
echo "eula=true" > /paper/eula.txt
COPY --from=plugin /felis-paper.jar /paper/plugins/felis-paper.jar
EXPOSE 25565
# nogui headless; the first boot generates server.properties (align online-mode /
# forwarding with the Velocity proxy afterwards — see README).
ENTRYPOINT ["java", "-jar", "paper.jar", "--nogui"]
+43
View File
@@ -0,0 +1,43 @@
# Lobby image (Paper + felis-paper)
The always-on **lobby** hub. `felis setup` provisions it as a system service
(`DesiredState=Running`, reaper-exempt) when `[velocity] lobby_image` is set in
`felis.toml`.
> **Code-only.** Not built by the Go CI. It compiles the `plugins/paper`
> `/menu` face and bundles it onto a Paper server.
## Topology & the one invariant
```
connect → login (limbo auth gate) → lobby (/menu hub) → target backend
```
The lobby is reached **only** when the login gate transfers an authenticated
player onward. It is never a fallback or waiting-park target — routing a fresh
connection to the lobby would drop the player past authentication. Felis enforces
this at every layer:
- the login system service has **no** fallback (refuse if down);
- the lobby and every user server fall back to **login**, never to the lobby;
- `buildSystemServer` refuses to construct any service whose fallback is `lobby`;
- `felis setup` prints the off-cluster Velocity wiring: default landing and
waiting-park both point at `login`.
## Build
```
docker build -f deploy/lobby/Dockerfile \
--build-arg PAPER_JAR_URL=https://<mirror>/paper-1.21.x-<build>.jar \
-t felis-lobby:demo .
docker save felis-lobby:demo | sudo k3s ctr images import -
# felis.toml → [velocity] lobby_image = "felis-lobby:demo"
sudo felis setup
```
## Configure (deployer's responsibility)
- Game port must be `25565` (the CRD `GamePort`).
- Align `online-mode` / player forwarding with the off-cluster Velocity proxy.
- The lobby speaks only the `felis:control` plugin-message channel; it holds no
felis-api token by design (spec §12).