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:
6 files changed
+361
-1
No files matched your search
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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 "$@"
|
||||
@@ -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"]
|
||||
@@ -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).
|
||||
Reference in new issue
Block a user