Merge pull request #19 from MliroLirrorsIngenuity/chore/issue-sweep
chore: work the tracker items that need no cluster
This commit was merged in pull request #19.
This commit is contained in:
13 files changed
+489
-78
No files matched your search
@@ -0,0 +1,8 @@
|
|||||||
|
# Everything in this repository is consumed on Linux: deploy/bootstrap.sh is embedded
|
||||||
|
# verbatim by bootstrap_asset.go and piped into `bash -s`, the Dockerfiles and entrypoints
|
||||||
|
# are read inside the images, and the operator ships YAML. Without eol=lf a Windows
|
||||||
|
# checkout under core.autocrlf=true hands all of them CRs.
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# The gradle wrappers' Windows launchers are the one thing that wants CRLF.
|
||||||
|
*.bat text eol=crlf
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Runs the checks on pull requests and on main.
|
||||||
|
#
|
||||||
|
# release.yml already runs `go vet` and `go test`, but only once a vX.Y.Z tag exists — by then
|
||||||
|
# a red change is on the release path and the only remedy is a new tag. This is the same gate
|
||||||
|
# moved to where it can still stop something, plus the panel suite, which nothing ran at all:
|
||||||
|
# the release goes through the Dockerfile, and the Dockerfile runs `npm run build`, never
|
||||||
|
# `npm test`.
|
||||||
|
#
|
||||||
|
# The push trigger is limited to main rather than every branch, for the reason release.yml is
|
||||||
|
# not repeated here: a branch with an open PR would otherwise run the whole suite twice per
|
||||||
|
# push, once for refs/heads/<branch> and once for refs/pull/N/merge. Those are different
|
||||||
|
# concurrency groups, so neither cancels the other, and this repository is private and billed
|
||||||
|
# for both. A branch with no PR open yet is the one case that loses coverage, and opening the
|
||||||
|
# PR is what asks for the answer.
|
||||||
|
name: ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
go:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- run: go vet ./...
|
||||||
|
- run: go test ./...
|
||||||
|
|
||||||
|
shell:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# bootstrap.sh is the only thing that ever runs on a fresh host, and nothing here can
|
||||||
|
# run it — it wants root, a package manager and k3s. Syntax plus the extracted-block
|
||||||
|
# checks in bootstrap_test.sh is the coverage that is reachable without a machine.
|
||||||
|
# Each file is parsed by the interpreter its own shebang names. A blanket `sh -n` is
|
||||||
|
# wrong and not obviously so: on a developer machine `sh` is usually bash and passes
|
||||||
|
# everything, while the runner's `sh` is dash and rejects bootstrap.sh at the first of
|
||||||
|
# its arrays. Honouring the shebang is what makes local and CI agree.
|
||||||
|
- name: Check shell syntax
|
||||||
|
run: |
|
||||||
|
for f in $(git ls-files '*.sh'); do
|
||||||
|
case "$(head -1 "$f")" in
|
||||||
|
*bash) bash -n "$f" || exit 1 ;;
|
||||||
|
*) sh -n "$f" || exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
- run: sh deploy/bootstrap_test.sh
|
||||||
|
|
||||||
|
panel:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# The Dockerfile's `FROM node:<major>` is the only place the panel's Node version is
|
||||||
|
# declared — there is no .nvmrc and no engines field. Reading it here rather than
|
||||||
|
# repeating the number keeps CI testing the version a release is actually built on;
|
||||||
|
# a second copy would drift silently the first time the image is bumped.
|
||||||
|
- name: Read the panel's Node version from the Dockerfile
|
||||||
|
id: node
|
||||||
|
run: |
|
||||||
|
version="$(sed -n 's/^FROM.*node:\([0-9][0-9]*\)-.*/\1/p' Dockerfile | head -1)"
|
||||||
|
[ -n "$version" ] || { echo "Dockerfile has no 'FROM ... node:<major>' line"; exit 1; }
|
||||||
|
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ steps.node.outputs.version }}
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: panel/package-lock.json
|
||||||
|
|
||||||
|
- run: npm ci
|
||||||
|
working-directory: panel
|
||||||
|
|
||||||
|
- run: npm test
|
||||||
|
working-directory: panel
|
||||||
|
|
||||||
|
- run: npm run typecheck
|
||||||
|
working-directory: panel
|
||||||
@@ -73,9 +73,15 @@ func TestLobbyLuckPermsWiringIsConsistent(t *testing.T) {
|
|||||||
// default it can inherit — and nothing else in the install would notice it missing. The failure
|
// default it can inherit — and nothing else in the install would notice it missing. The failure
|
||||||
// surfaces only when a legacy player joins, on a host that installed cleanly.
|
// surfaces only when a legacy player joins, on a host that installed cleanly.
|
||||||
func TestBootstrapPinsViaBlockConnectionsOff(t *testing.T) {
|
func TestBootstrapPinsViaBlockConnectionsOff(t *testing.T) {
|
||||||
// go:embed takes the working tree verbatim, and this repository pins no eol attribute, so
|
// go:embed takes the working tree verbatim, so the eol attribute is what keeps a Windows
|
||||||
// a Windows checkout embeds CRLF. Only the assertion spanning a line break below cares.
|
// checkout from compiling CRs into the installer. Assert it rather than normalizing them
|
||||||
script := strings.ReplaceAll(BootstrapScript(), "\r\n", "\n")
|
// away: the only assertion that would otherwise notice is the one spanning a line break
|
||||||
|
// below, and it would report a missing pin instead of the line endings.
|
||||||
|
script := BootstrapScript()
|
||||||
|
if strings.Contains(script, "\r\n") {
|
||||||
|
t.Fatal("embedded bootstrap.sh has CRLF line endings; .gitattributes pins *.sh to LF " +
|
||||||
|
"and this script is piped into `bash -s` on a Linux host")
|
||||||
|
}
|
||||||
|
|
||||||
const key = "serverside-blockconnections"
|
const key = "serverside-blockconnections"
|
||||||
if !strings.Contains(script, key+": false") {
|
if !strings.Contains(script, key+": false") {
|
||||||
|
|||||||
+10
-2
@@ -13,8 +13,16 @@ package main
|
|||||||
// off-loopback with -listen; see the flag below for why that is an explicit opt-in.
|
// off-loopback with -listen; see the flag below for why that is an explicit opt-in.
|
||||||
//
|
//
|
||||||
// This is the no-database delivery of the identical brain `felis api` mounts through its
|
// This is the no-database delivery of the identical brain `felis api` mounts through its
|
||||||
// route table (internal/api.HasJoinedHandler). `felis setup --nano` / the bootstrap nano
|
// route table (internal/api.HasJoinedHandler). The bootstrap installer's nano choice — its
|
||||||
// choice install this as the runtime service; here it just serves.
|
// `[2] Felis-nano` prompt, or FELIS_INSTALL_MODE=nano — installs this as the
|
||||||
|
// felis-nano.service unit; here it just serves.
|
||||||
|
//
|
||||||
|
// There is deliberately no `felis setup --nano`. setup re-images the host through the full
|
||||||
|
// bootstrap TUI and carries no install-mode parameter anywhere (nothing in Go reads or sets
|
||||||
|
// FELIS_INSTALL_MODE), so such a flag would either re-run the installer — which is what
|
||||||
|
// pointing at the installer already does — or tear a full install down into a nano one,
|
||||||
|
// which is an uninstall, not a flag. This is the same reasoning that makes setup's --dev
|
||||||
|
// refuse and name the installer rather than pretend to choose a channel.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|||||||
+62
-8
@@ -30,6 +30,14 @@
|
|||||||
# FELIS_INSTALL_MODE full|nano — skip the prompt (default: ask on a tty, else full)
|
# FELIS_INSTALL_MODE full|nano — skip the prompt (default: ask on a tty, else full)
|
||||||
# FELIS_NANO_LISTEN listen addr for `felis nano` (default: 127.0.0.1:8081 — loopback
|
# FELIS_NANO_LISTEN listen addr for `felis nano` (default: 127.0.0.1:8081 — loopback
|
||||||
# only; set a private-network IP to serve an off-host proxy)
|
# only; set a private-network IP to serve an off-host proxy)
|
||||||
|
# FELIS_LEGACY_FORWARDING_SERVERS comma-separated backends that receive their identity
|
||||||
|
# through the handshake address instead of modern forwarding
|
||||||
|
# (default: legacy18). Read once at Velocity start, so changing it
|
||||||
|
# means re-running this script and restarting the proxy.
|
||||||
|
# FELIS_VELOCITY_FORK_JAR path to a Felis-Legacy Velocity fork build to install as the
|
||||||
|
# proxy instead of the stock download (default: unset, stock).
|
||||||
|
# FELIS_VELOCITY_FORK_JAR_SHA256 expected sha256 of that jar. REQUIRED whenever the jar
|
||||||
|
# above is set; the install refuses on a mismatch.
|
||||||
# FELIS_GO_VERSION Go toolchain used to build the nano binary (default: 1.26.4)
|
# FELIS_GO_VERSION Go toolchain used to build the nano binary (default: 1.26.4)
|
||||||
# FELIS_REPO_URL git URL to build from (raw script mode only)
|
# FELIS_REPO_URL git URL to build from (raw script mode only)
|
||||||
# FELIS_VERSION_BOOTSTRAP release|dev — which version to install (default: release).
|
# FELIS_VERSION_BOOTSTRAP release|dev — which version to install (default: release).
|
||||||
@@ -95,6 +103,11 @@ INSTALL_MODE="${FELIS_INSTALL_MODE:-}"
|
|||||||
# own players stop getting in. Same-host Velocity reaches 127.0.0.1 fine; a proxy on
|
# own players stop getting in. Same-host Velocity reaches 127.0.0.1 fine; a proxy on
|
||||||
# another machine must opt in explicitly with FELIS_NANO_LISTEN=<private-ip>:8081.
|
# another machine must opt in explicitly with FELIS_NANO_LISTEN=<private-ip>:8081.
|
||||||
FELIS_NANO_LISTEN="${FELIS_NANO_LISTEN:-127.0.0.1:8081}"
|
FELIS_NANO_LISTEN="${FELIS_NANO_LISTEN:-127.0.0.1:8081}"
|
||||||
|
# Backends that take their forwarded identity through the handshake address instead of
|
||||||
|
# proxy-wide modern forwarding. See write_velocity_service for why a protocol-47 backend
|
||||||
|
# needs this. Overridable because adding a second 1.8 backend otherwise means editing this
|
||||||
|
# script; it is still a restart-time list, not one that follows the CRs.
|
||||||
|
FELIS_LEGACY_FORWARDING_SERVERS="${FELIS_LEGACY_FORWARDING_SERVERS:-legacy18}"
|
||||||
FELIS_GO_VERSION="${FELIS_GO_VERSION:-1.26.4}"
|
FELIS_GO_VERSION="${FELIS_GO_VERSION:-1.26.4}"
|
||||||
PKG_LOCK_TIMEOUT="${PKG_LOCK_TIMEOUT:-${APT_LOCK_TIMEOUT:-900}}"
|
PKG_LOCK_TIMEOUT="${PKG_LOCK_TIMEOUT:-${APT_LOCK_TIMEOUT:-900}}"
|
||||||
APT_LOCK_TIMEOUT="${APT_LOCK_TIMEOUT:-$PKG_LOCK_TIMEOUT}"
|
APT_LOCK_TIMEOUT="${APT_LOCK_TIMEOUT:-$PKG_LOCK_TIMEOUT}"
|
||||||
@@ -123,9 +136,20 @@ FELIS_VELOCITY_VERSION="${FELIS_VELOCITY_VERSION:-3.5.1}"
|
|||||||
#
|
#
|
||||||
# Opt-in because it is unmeasured where it counts: FL-008's probe runs offline-mode
|
# Opt-in because it is unmeasured where it counts: FL-008's probe runs offline-mode
|
||||||
# against a stub, and this jar would carry every real Mojang session on the server.
|
# against a stub, and this jar would carry every real Mojang session on the server.
|
||||||
# The build lives in Felis-Legacy and is not byte-reproducible, so there is no digest
|
|
||||||
# to pin here — the jar is trusted because that probe certified the build.
|
|
||||||
FELIS_VELOCITY_FORK_JAR="${FELIS_VELOCITY_FORK_JAR:-}"
|
FELIS_VELOCITY_FORK_JAR="${FELIS_VELOCITY_FORK_JAR:-}"
|
||||||
|
# Expected sha256 of that jar, REQUIRED whenever it is set. Case and internal spaces are
|
||||||
|
# ignored, so whatever sha256sum, Get-FileHash or certutil printed can be pasted as-is.
|
||||||
|
# No digest is hardcoded here:
|
||||||
|
# the build lives in Felis-Legacy and has never been reproduced on a second machine, so
|
||||||
|
# any constant this script carried would pin one machine's output rather than the fork.
|
||||||
|
#
|
||||||
|
# So this is not a supply-chain signature and does not pretend to be one — an operator
|
||||||
|
# who can write the jar can write this value too. What it does buy: a path is not an
|
||||||
|
# identity, and every re-run of this script re-checks it. A truncated copy, a stale build
|
||||||
|
# left at the same path, or the two-patch jar where the three-patch one was meant all
|
||||||
|
# change the digest and stop the install. Naming the digest once is what turns "whatever
|
||||||
|
# is at that path today" into one specific build.
|
||||||
|
FELIS_VELOCITY_FORK_JAR_SHA256="${FELIS_VELOCITY_FORK_JAR_SHA256:-}"
|
||||||
# Temurin 25: Velocity 3.5 needs 21+, and 25 is also what a future Velocity 4 requires,
|
# Temurin 25: Velocity 3.5 needs 21+, and 25 is also what a future Velocity 4 requires,
|
||||||
# so the runtime does not have to move again when the pin does. Distro JDK packaging is
|
# so the runtime does not have to move again when the pin does. Distro JDK packaging is
|
||||||
# a lottery across four package managers — a tarball is one code path everywhere (same
|
# a lottery across four package managers — a tarball is one code path everywhere (same
|
||||||
@@ -1462,12 +1486,31 @@ install_jre() {
|
|||||||
|
|
||||||
install_velocity() {
|
install_velocity() {
|
||||||
install_jre
|
install_jre
|
||||||
local url tmp
|
local url tmp have want
|
||||||
prepare_velocity_layout
|
prepare_velocity_layout
|
||||||
if [ -n "$FELIS_VELOCITY_FORK_JAR" ]; then
|
if [ -n "$FELIS_VELOCITY_FORK_JAR" ]; then
|
||||||
[ -f "$FELIS_VELOCITY_FORK_JAR" ] \
|
[ -f "$FELIS_VELOCITY_FORK_JAR" ] \
|
||||||
|| die "FELIS_VELOCITY_FORK_JAR is not a readable file: ${FELIS_VELOCITY_FORK_JAR}"
|
|| die "FELIS_VELOCITY_FORK_JAR is not a readable file: ${FELIS_VELOCITY_FORK_JAR}"
|
||||||
log "installing the Felis-Legacy Velocity fork from ${FELIS_VELOCITY_FORK_JAR}"
|
# Hash stdin, never the path — same reason as install_via_plugins: sha256sum escapes its
|
||||||
|
# output line for a filename carrying a backslash or a newline, and the leading "\" that
|
||||||
|
# adds would fail every comparison below.
|
||||||
|
have="$(sha256sum <"$FELIS_VELOCITY_FORK_JAR" | cut -d' ' -f1)"
|
||||||
|
# Refuse rather than warn. This jar is the proxy every player connects through, and a
|
||||||
|
# warning in an install log is not a gate. The digest is printed so the first run after
|
||||||
|
# a deliberate rebuild is one copy-paste, not an investigation.
|
||||||
|
[ -n "$FELIS_VELOCITY_FORK_JAR_SHA256" ] || die \
|
||||||
|
"FELIS_VELOCITY_FORK_JAR_SHA256 is required whenever FELIS_VELOCITY_FORK_JAR is set.
|
||||||
|
The jar at that path hashes to ${have}.
|
||||||
|
Check that against the build you meant to install, then re-run with
|
||||||
|
FELIS_VELOCITY_FORK_JAR_SHA256=${have}"
|
||||||
|
# Normalise the operator's digest before comparing. sha256sum prints lowercase, but the
|
||||||
|
# build host is often Windows, where Get-FileHash prints uppercase and certutil has
|
||||||
|
# shipped both with and without spaces between the bytes. All three name the same jar,
|
||||||
|
# so comparing raw would refuse two of the three spellings and word it as tampering.
|
||||||
|
want="$(printf '%s' "$FELIS_VELOCITY_FORK_JAR_SHA256" | tr -d '[:space:]' | tr 'A-Z' 'a-z')"
|
||||||
|
[ "$have" = "$want" ] || die \
|
||||||
|
"FELIS_VELOCITY_FORK_JAR checksum mismatch: got ${have}, expected ${want}"
|
||||||
|
log "installing the Felis-Legacy Velocity fork from ${FELIS_VELOCITY_FORK_JAR} (sha256 ${have})"
|
||||||
atomic_install_file "$FELIS_VELOCITY_FORK_JAR" "${VELOCITY_DIR}/velocity.jar" 0644 root root
|
atomic_install_file "$FELIS_VELOCITY_FORK_JAR" "${VELOCITY_DIR}/velocity.jar" 0644 root root
|
||||||
else
|
else
|
||||||
log "resolving the newest Velocity ${FELIS_VELOCITY_VERSION} build"
|
log "resolving the newest Velocity ${FELIS_VELOCITY_VERSION} build"
|
||||||
@@ -1591,9 +1634,20 @@ install_velocity_service() {
|
|||||||
# behind ViaVersion, which strips modern forwarding's login-plugin-message when it down-translates
|
# behind ViaVersion, which strips modern forwarding's login-plugin-message when it down-translates
|
||||||
# the proxy->backend pipeline to protocol 47; only the handshake field survives Via. The Felis
|
# the proxy->backend pipeline to protocol 47; only the handshake field survives Via. The Felis
|
||||||
# fork reads this list from -Dfelis.legacy-forwarding.servers and forwards those servers legacy;
|
# fork reads this list from -Dfelis.legacy-forwarding.servers and forwards those servers legacy;
|
||||||
# every other backend keeps modern+secret untouched. v1 hardcodes the one legacy backend; the
|
# every other backend keeps modern+secret untouched.
|
||||||
# upgrade path is to have the operator render this list from the MinecraftServer CRs.
|
#
|
||||||
local legacy_forwarding_servers="legacy18"
|
# The list is a JVM system property, so it is fixed for the life of the proxy process and a
|
||||||
|
# change needs a Velocity restart. FELIS_LEGACY_FORWARDING_SERVERS makes that reachable
|
||||||
|
# without editing this script, which is as far as a startup property can go. Having it follow
|
||||||
|
# the MinecraftServer CRs instead is a larger change: the forwarding decision lives in the
|
||||||
|
# fork's patch to Velocity core, not in the Felis plugin, so core would need to read state the
|
||||||
|
# plugin owns and refreshes.
|
||||||
|
#
|
||||||
|
# The -D below is double-quoted in ExecStart on purpose. The fork trims each element, so it
|
||||||
|
# accepts "legacy18, legacy112", but systemd splits ExecStart on whitespace before java ever
|
||||||
|
# sees it -- unquoted, that spelling would hand java a stray "legacy112" argument and the unit
|
||||||
|
# would not start. Quoting keeps the whole property one argv item.
|
||||||
|
local legacy_forwarding_servers="${FELIS_LEGACY_FORWARDING_SERVERS}"
|
||||||
cat > "$VELOCITY_SERVICE" <<EOF
|
cat > "$VELOCITY_SERVICE" <<EOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Felis Velocity proxy (Mojang authentication + modern forwarding)
|
Description=Felis Velocity proxy (Mojang authentication + modern forwarding)
|
||||||
@@ -1605,7 +1659,7 @@ Type=simple
|
|||||||
User=${VELOCITY_USER}
|
User=${VELOCITY_USER}
|
||||||
Group=${VELOCITY_USER}
|
Group=${VELOCITY_USER}
|
||||||
WorkingDirectory=${VELOCITY_DIR}
|
WorkingDirectory=${VELOCITY_DIR}
|
||||||
ExecStart=${JRE_DIR}/bin/java -Xms512M -Xmx1G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:+AlwaysPreTouch -Dmojang.sessionserver=http://${api_ip}:8081/session/minecraft/hasJoined -Dfelis.legacy-forwarding.servers=${legacy_forwarding_servers} -jar ${VELOCITY_DIR}/velocity.jar
|
ExecStart=${JRE_DIR}/bin/java -Xms512M -Xmx1G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:+AlwaysPreTouch -Dmojang.sessionserver=http://${api_ip}:8081/session/minecraft/hasJoined "-Dfelis.legacy-forwarding.servers=${legacy_forwarding_servers}" -jar ${VELOCITY_DIR}/velocity.jar
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
NoNewPrivileges=yes
|
NoNewPrivileges=yes
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Checks for deploy/bootstrap.sh. Run it as: sh deploy/bootstrap_test.sh
|
||||||
|
#
|
||||||
|
# The script it tests cannot be run here — it wants root, a package manager, k3s and the
|
||||||
|
# network — so each case extracts the block it is about out of bootstrap.sh verbatim and runs
|
||||||
|
# that with die/log stubbed. Extraction rather than a transcribed copy is the point: a copy
|
||||||
|
# passes forever after someone edits the original.
|
||||||
|
set -u
|
||||||
|
|
||||||
|
BS="${1:-$(dirname "$0")/bootstrap.sh}"
|
||||||
|
[ -f "$BS" ] || { echo "no such script: $BS"; exit 1; }
|
||||||
|
fails=0
|
||||||
|
|
||||||
|
expect() { # label needle haystack
|
||||||
|
case "$3" in
|
||||||
|
*"$2"*) echo "PASS $1" ;;
|
||||||
|
*) echo "FAIL $1: expected <$2> in:"; echo "$3"; fails=$((fails + 1)) ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- the FELIS_VELOCITY_FORK_JAR digest gate -------------------------------------------
|
||||||
|
# This jar becomes the proxy every player connects through, so the interesting cases are the
|
||||||
|
# two refusals, not the happy path.
|
||||||
|
|
||||||
|
gate="$(awk '/have="\$\(sha256sum <"\$FELIS_VELOCITY_FORK_JAR"/,/^ log "installing the Felis-Legacy/' "$BS")"
|
||||||
|
[ -n "$gate" ] || { echo "FAIL: no fork-jar digest gate found in $BS"; exit 1; }
|
||||||
|
# awk runs an unmatched end pattern to EOF, which would quietly pipe the rest of bootstrap.sh
|
||||||
|
# into the `sh -c` below. The emptiness check above only catches a broken start pattern.
|
||||||
|
[ "$(printf '%s\n' "$gate" | wc -l)" -lt 40 ] \
|
||||||
|
|| { echo "FAIL: the extracted block is not the gate -- did its last line move?"; exit 1; }
|
||||||
|
|
||||||
|
jar="$(mktemp)"
|
||||||
|
trap 'rm -f "$jar"' EXIT
|
||||||
|
printf 'stand-in for a fork build\n' > "$jar"
|
||||||
|
want="$(sha256sum <"$jar" | cut -d' ' -f1)"
|
||||||
|
|
||||||
|
run_gate() { # digest
|
||||||
|
FELIS_VELOCITY_FORK_JAR="$jar" FELIS_VELOCITY_FORK_JAR_SHA256="$1" sh -c '
|
||||||
|
die() { printf "DIE: %s\n" "$*"; exit 1; }
|
||||||
|
log() { printf "LOG: %s\n" "$*"; }
|
||||||
|
'"$gate"
|
||||||
|
}
|
||||||
|
|
||||||
|
out="$(run_gate '')"
|
||||||
|
expect "fork jar without a digest is refused" "DIE: FELIS_VELOCITY_FORK_JAR_SHA256 is required" "$out"
|
||||||
|
expect "the refusal names the jar's real digest" "$want" "$out"
|
||||||
|
|
||||||
|
out="$(run_gate 'deadbeef')"
|
||||||
|
expect "a mismatched digest is refused" "checksum mismatch: got ${want}, expected deadbeef" "$out"
|
||||||
|
|
||||||
|
out="$(run_gate "$want")"
|
||||||
|
expect "the matching digest installs" "LOG: installing the Felis-Legacy Velocity fork" "$out"
|
||||||
|
expect "the install line records the digest" "(sha256 ${want})" "$out"
|
||||||
|
|
||||||
|
# The build host is usually Windows, where nothing prints the digest the way sha256sum does:
|
||||||
|
# Get-FileHash returns uppercase and certutil has shipped the bytes space-separated. Feeding
|
||||||
|
# the gate its own output can never catch that -- these two cases are the operator's paste.
|
||||||
|
out="$(run_gate "$(printf '%s' "$want" | tr 'a-z' 'A-Z')")"
|
||||||
|
expect "an uppercase digest is the same digest" "LOG: installing the Felis-Legacy Velocity fork" "$out"
|
||||||
|
|
||||||
|
out="$(run_gate "$(printf '%s' "$want" | sed 's/../& /g')")"
|
||||||
|
expect "a space-separated digest is the same digest" "LOG: installing the Felis-Legacy Velocity fork" "$out"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------------------
|
||||||
|
if [ "$fails" -eq 0 ]; then
|
||||||
|
echo "ALL PASS"
|
||||||
|
else
|
||||||
|
echo "$fails FAILED"
|
||||||
|
fi
|
||||||
|
exit "$fails"
|
||||||
@@ -269,10 +269,6 @@ spec:
|
|||||||
storage:
|
storage:
|
||||||
description: Storage configures the world PVC.
|
description: Storage configures the world PVC.
|
||||||
properties:
|
properties:
|
||||||
retainOnDelete:
|
|
||||||
description: RetainOnDelete keeps the PVC when the MinecraftServer
|
|
||||||
is deleted.
|
|
||||||
type: boolean
|
|
||||||
size:
|
size:
|
||||||
description: Size is the requested PVC capacity (e.g. "10Gi").
|
description: Size is the requested PVC capacity (e.g. "10Gi").
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Deferred integration seams
|
||||||
|
|
||||||
|
`INTEGRATION-ONLY` and `KNOWN-LIMITATION` are grep-able markers in the Go source.
|
||||||
|
This file is the index of what each one currently means, so that reading the
|
||||||
|
unfinished face of the system does not require re-deriving it from 34 comment
|
||||||
|
sites.
|
||||||
|
|
||||||
|
It exists for two reasons. The markers do not all mean the same thing — four
|
||||||
|
distinct states share them, and "declared, nothing implements it" reads exactly
|
||||||
|
like "implemented, but its I/O cannot be exercised from this repo". And a marker
|
||||||
|
outlives the condition it describes: two of them were stale when this index was
|
||||||
|
first assembled, both claiming as future work something that had already shipped.
|
||||||
|
|
||||||
|
The pattern here is the one `internal/updater/doc.go` already uses for its own
|
||||||
|
package — state the verification boundary in buckets, so a green test suite is not
|
||||||
|
mistaken for a finished integration. This file is the same idea across the whole
|
||||||
|
tree.
|
||||||
|
|
||||||
|
## The marker collides with a different vocabulary in troubleshooting.md
|
||||||
|
|
||||||
|
`docs/troubleshooting.md` uses `[INTEGRATION-ONLY]` for something else, defined in
|
||||||
|
its own opening at `:19`: the symptom is produced by the kubelet, kaniko or a live
|
||||||
|
handshake, so it cannot be reproduced from the repository. Those twelve marks say
|
||||||
|
where a failure comes from. They are not unfinished work and are not indexed below.
|
||||||
|
A grep across `*.md` and `*.go` returns both sets; only the Go ones are seams.
|
||||||
|
|
||||||
|
## Declared, nothing implements it
|
||||||
|
|
||||||
|
- `internal/updates/seams.go:32` — `Notifier`. `internal/mail` sends OTP over SMTP,
|
||||||
|
but nothing adapts it to this interface and no in-game channel exists. `felis
|
||||||
|
update` passes nil deliberately: a human typing the command is the notification.
|
||||||
|
- `internal/updates/seams.go:43` — `Applier`. Nothing applies an update anywhere. A
|
||||||
|
nil applier is not silent — `Run` records `errNoApplier` against every planned
|
||||||
|
apply, so a mis-scheduled apply is loud rather than lost.
|
||||||
|
- `internal/updater/gatherer_integration.go:22` — the two current-version seams
|
||||||
|
`NewSysGatherer` leaves nil, for the in-cluster path: the k8s read of the
|
||||||
|
control-plane Deployment image, and the Velocity jar inspection. Both are answered
|
||||||
|
on the host path (see "Built" below), so this gap is specific to a caller that has
|
||||||
|
a cluster client instead of the node.
|
||||||
|
- `internal/api/handlers_updates.go:21,28` — the maintenance window persists and the
|
||||||
|
API serves it, but the in-cluster CronJob that would hand a real window to a runner
|
||||||
|
does not exist. `felis update` runs with a zero window, under which every
|
||||||
|
`Scheduled` component degrades to a notify, so no path can currently claim an
|
||||||
|
apply is under way.
|
||||||
|
- `internal/submit/blobstore.go:40` — the uploads PVC is mounted into felis-api but
|
||||||
|
not into the Kaniko build Pod, so a submitted context is durable at the derived
|
||||||
|
location without yet being readable by the build that consumes it.
|
||||||
|
|
||||||
|
## Built; only its I/O is unverifiable from this repo
|
||||||
|
|
||||||
|
Code exists and is unit-tested against fakes. What is missing is a host, a cluster
|
||||||
|
or a real upstream account to run it against — not an implementation.
|
||||||
|
|
||||||
|
- `cmd/felis/tui_edge_apply.go:246,274,295` — the `nft` edge fence, its idempotent
|
||||||
|
teardown, and the cloudflared invocation.
|
||||||
|
- `internal/cfsetup/runner.go:18` and `internal/cfsetup/cfsetup.go:329` — the real
|
||||||
|
Cloudflare Tunnel and Access API calls; `internal/cfsetup/cfsetup_test.go:11`
|
||||||
|
drives the whole flow through a fake.
|
||||||
|
- `internal/api/console.go:39`, `internal/api/logstream.go:236`,
|
||||||
|
`internal/api/logstream.go:306`, `internal/fileedit/k8sjobs.go:45` — each needs a
|
||||||
|
live cluster (RCON, `pods/log` follow, a Job).
|
||||||
|
- `internal/api/handlers_access.go:170,490` — parsing real vanilla and LuckPerms
|
||||||
|
command output.
|
||||||
|
|
||||||
|
## Deliberately accepted, not scheduled to close
|
||||||
|
|
||||||
|
These are decisions, not backlog. Each names the condition under which it would be
|
||||||
|
worth revisiting.
|
||||||
|
|
||||||
|
- `internal/api/pgrepo.go:281` — the quota check and `ClaimServer` are two statements
|
||||||
|
(audit #4 TOCTOU). Closeable only against a real Postgres.
|
||||||
|
- `internal/api/api.go:671` — `cooldownLimiter` is process-local, so across N api
|
||||||
|
replicas a caller could draw up to N OTP codes per window. The intra-replica burst
|
||||||
|
is closed; cross-replica bounding needs a shared store, out of scope for a
|
||||||
|
single-replica install.
|
||||||
|
- `internal/submit/submit.go:436` and `internal/submit/submit_test.go:351` — a
|
||||||
|
post-CAS `Approve`
|
||||||
|
failure leaves a row indistinguishable from the benign case, so `Approve` returns a
|
||||||
|
distinct error naming the running build rather than allowing a blind re-drive that
|
||||||
|
would double-push. The alternative ordering is worse.
|
||||||
|
- `cmd/felis/tui_edge_apply.go:246`, second marker on the same site — the fence
|
||||||
|
targets nftables. On a firewalld host a reload can flush the standalone table;
|
||||||
|
firewalld-native coordination is not handled. A missing `nft` binary fails loud
|
||||||
|
rather than leaving the port open.
|
||||||
|
- `internal/api/handlers_account.go:169` — the reclaim "start fresh vs inherit"
|
||||||
|
choice is CODE-ONLY on the Java/Velocity side; the link-status endpoint reports
|
||||||
|
link completion only and does not surface it.
|
||||||
|
|
||||||
|
## Wired since the marker was written
|
||||||
|
|
||||||
|
- `internal/api/handlers_email_otp.go:54,223,227` and `internal/api/api.go:84` —
|
||||||
|
SMTP shipped on
|
||||||
|
2026-07-20 (`internal/mail`, wired at `cmd/felis/api.go:264`). The nil-`Mailer`
|
||||||
|
branch that logs the code server-side is a runtime fallback for an install with no
|
||||||
|
`[smtp]` section, not an unbuilt feature. The comments are accurate; the reading
|
||||||
|
"Felis cannot send mail" is not.
|
||||||
|
- `internal/config/config.go:117` — was stale. It described the upload transport as a
|
||||||
|
deferred integration after both backends had shipped (`LocalContextStore`,
|
||||||
|
`S3ContextStore`, selected in `cmd/felis/api.go` by the shape of the configured
|
||||||
|
base). Corrected in the change that added this file; what remains deferred is only
|
||||||
|
Kaniko's read, indexed above.
|
||||||
|
- `internal/updater/doc.go:44` — was stale. Its REMAINING INTEGRATION bullet listed
|
||||||
|
the `felis update` CLI and the off-cluster Velocity jar read, both of which exist
|
||||||
|
(`cmd/felis/update.go`, `internal/updater/gatherer_host.go`). Corrected in the same
|
||||||
|
change; the two nil seams it also names are real and remain above.
|
||||||
|
|
||||||
|
## Recorded outside the code
|
||||||
|
|
||||||
|
- `deploy/limbo/README.md:139` — no NetworkPolicy locks the minecraft-namespace
|
||||||
|
egress or the control-namespace ingress today, which is why the login pod reaches
|
||||||
|
`felis-api-internal:8081`. This is a conditional obligation rather than a seam: if
|
||||||
|
a future deployment adds either lock, it must also open that path. Spec v4.1 §21
|
||||||
|
asks for those policies; `cmd/felis/manifests.go` renders the game-port one.
|
||||||
+76
-34
@@ -20,8 +20,8 @@ graded for how far the in-repo Go test suite proves the behaviour:
|
|||||||
containerd, Postgres, or the network, not by Felis Go code; you will see it in
|
containerd, Postgres, or the network, not by Felis Go code; you will see it in
|
||||||
`kubectl describe` / pod logs, never in `MinecraftServer.status`.
|
`kubectl describe` / pod logs, never in `MinecraftServer.status`.
|
||||||
- **[INERT]** — the configuration field exists in the CRD but no controller
|
- **[INERT]** — the configuration field exists in the CRD but no controller
|
||||||
reads it. Tuning it does nothing. These are the highest-value traps and are
|
reads it. Tuning it does nothing. §12 lists the one field this still applies
|
||||||
collected in §10.
|
to, alongside the fields that *are* read and the condition each depends on.
|
||||||
|
|
||||||
The operator never invents the parent domain; routing identity is
|
The operator never invents the parent domain; routing identity is
|
||||||
`spec.subdomain` under the deployment zone. Examples below use
|
`spec.subdomain` under the deployment zone. Examples below use
|
||||||
@@ -32,14 +32,21 @@ concrete host.
|
|||||||
|
|
||||||
## 1. Server is stuck in `Starting` and never becomes `Running`
|
## 1. Server is stuck in `Starting` and never becomes `Running`
|
||||||
|
|
||||||
`MinecraftServer.status.phase` stays `Starting`. The single most important fact:
|
`MinecraftServer.status.phase` stays `Starting`. A start that never succeeds is
|
||||||
**the operator has no start timeout.** A start that never succeeds loops in
|
requeued every 5s until one of the two startup budgets expires, then escalated
|
||||||
`Starting`, requeued every 5s, **forever** — it is never auto-escalated to
|
to `Failed` — `StartupTimeout` if the pod never passed TCP readiness,
|
||||||
`Failed`. [GO-TESTED: `TestReconcileRunning_RconProbeFailureStaysStarting`
|
`ReadinessTimeout` if the RCON probe never succeeded. Both default to **300s**
|
||||||
asserts the phase stays `Starting`.] Do **not** reach for
|
when `spec.startup.timeoutSeconds` / `spec.startup.readinessTimeoutSeconds` are
|
||||||
`spec.startup.timeoutSeconds` / `spec.startup.readinessTimeoutSeconds` — both are
|
unset or `0`, and both are measured from `status.startRequestedAt`.
|
||||||
[INERT] (§10). A hung start must be diagnosed from pod state, not from
|
[GO-TESTED: `TestReconcileRunning_StartupTimeoutConvertsToFailed`,
|
||||||
`MinecraftServer.status`.
|
`TestReconcileRunning_ReadinessTimeoutConvertsToFailed`.]
|
||||||
|
|
||||||
|
So `Starting` seen *once* is normal and
|
||||||
|
`TestReconcileRunning_RconProbeFailureStaysStarting` asserts exactly that — a
|
||||||
|
single failed probe must not flap the phase. `Starting` seen for longer than the
|
||||||
|
budget means the reconcile loop is not running at all; check the operator's own
|
||||||
|
logs before tuning anything. Either way the underlying cause is diagnosed from
|
||||||
|
pod state, not from `MinecraftServer.status`.
|
||||||
|
|
||||||
First, read the condition reason:
|
First, read the condition reason:
|
||||||
|
|
||||||
@@ -111,8 +118,10 @@ message is the verbatim dial error:
|
|||||||
port yet, RCON is disabled in `server.properties`, or `spec.rcon.port`
|
port yet, RCON is disabled in `server.properties`, or `spec.rcon.port`
|
||||||
(default 25575) is wrong. [INTEGRATION-ONLY for the live handshake.]
|
(default 25575) is wrong. [INTEGRATION-ONLY for the live handshake.]
|
||||||
|
|
||||||
The per-probe timeout is a fixed 5s in code — it is **not** derived from
|
The per-probe timeout is a fixed 5s in code (`prober.go:45`, shortened further if
|
||||||
`spec.startup.readinessTimeoutSeconds` ([INERT]).
|
the reconcile context has a nearer deadline). It is **not** derived from
|
||||||
|
`spec.startup.readinessTimeoutSeconds`, which is the deadline for the whole
|
||||||
|
start, not for one probe — see §12.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -463,14 +472,35 @@ store paths are [INTEGRATION-ONLY].
|
|||||||
|
|
||||||
## 11. Idle auto-stop never fires; player count always shows 0
|
## 11. Idle auto-stop never fires; player count always shows 0
|
||||||
|
|
||||||
**Idle auto-stop is entirely unimplemented in the operator.** `spec.idle.*`
|
Both are implemented, and both hang off the same switch: **`spec.rcon.enabled`**.
|
||||||
([INERT], §12) is read by no controller, and no idle controller is registered.
|
Check it first.
|
||||||
An empty `Running` server stays `Running`.
|
|
||||||
|
|
||||||
Relatedly, `status.players.online` is **permanently 0**: the only writer of
|
```sh
|
||||||
`status.players` zeroes it on stop, and the RCON prober only dials + closes — it
|
kubectl get minecraftserver <name> -o jsonpath='{.spec.rcon.enabled}'
|
||||||
never runs `list`. Any panel reading `status.players.online` will always show
|
```
|
||||||
empty. Do not build alerting on it, and do not expect "empty server" automation.
|
|
||||||
|
The player tally is a by-product of the RCON readiness probe — `prober.go:63`
|
||||||
|
runs `list` on the same connection that just authenticated, and `parseListReply`
|
||||||
|
extracts the tally from `There are (\d+) of a max of (\d+) players online`. With
|
||||||
|
RCON disabled the probe never runs, `players` keeps its zero value, and
|
||||||
|
`markRunningReady` (`reconciler.go:413`) writes that zero into
|
||||||
|
`status.players.online`. So a permanent 0 means "never sampled", not "nobody
|
||||||
|
online".
|
||||||
|
|
||||||
|
Idle auto-stop (`reconciler.go:175`) reads that same tally, which is why it
|
||||||
|
carries the RCON condition explicitly:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if server.Spec.Rcon.Enabled && server.Spec.Idle.AutoStopEnabled && server.Spec.Idle.EmptySecondsBeforeStop > 0 {
|
||||||
|
```
|
||||||
|
|
||||||
|
The comment above it says why: with RCON off the zero tally "would read as
|
||||||
|
'empty' and use to stop a server full of people". So the guard is deliberate —
|
||||||
|
enabling `spec.idle.*` without RCON is a no-op by design, not a missing feature.
|
||||||
|
|
||||||
|
Both fields set and still nothing happens? Then the probe is failing rather than
|
||||||
|
disabled: the server would be stuck in `Starting` with `RconNotReachable`
|
||||||
|
(`reconciler.go:156`), which is §1's symptom, not this one.
|
||||||
|
|
||||||
Note the reaper's `last_active_at` (§10) is a *different* subsystem (Postgres
|
Note the reaper's `last_active_at` (§10) is a *different* subsystem (Postgres
|
||||||
business layer, bumped by join events) — it keeps worlds alive against the
|
business layer, bumped by join events) — it keeps worlds alive against the
|
||||||
@@ -478,22 +508,21 @@ reaper, but it does **not** auto-stop empty running servers.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. Inert configuration fields (highest-value traps)
|
## 12. A configuration field seems to be ignored
|
||||||
|
|
||||||
These CRD fields exist and validate, but **no controller reads them.** Setting
|
Every field below is read by a controller. What varies is the condition that
|
||||||
them has **no effect**. Verified by grep: each appears only in the type
|
decides whether setting it does anything.
|
||||||
definition and its deepcopy, never in a controller.
|
|
||||||
|
|
||||||
| Field | What you might expect | Reality |
|
| Field | What you might expect | Reality |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `spec.startup.timeoutSeconds` | Start budget before `Failed` | **[INERT]** — no Starting→Failed timeout exists; a hung start loops forever (§1) |
|
| `spec.startup.timeoutSeconds` | Start budget before `Failed` | Read by `startupTimedOut` (`reconciler.go:479`), called at `:126`. `0` or unset falls back to **300s**, then `markFailed("StartupTimeout")` |
|
||||||
| `spec.startup.readinessTimeoutSeconds` | First-probe budget | **[INERT]** — probe timeout is a fixed 5s in code |
|
| `spec.startup.readinessTimeoutSeconds` | First-probe budget | Read by `readinessTimedOut` (`reconciler.go:490`), called at `:157`. `0` or unset falls back to **300s**, then `markFailed("ReadinessTimeout")`. Not to be confused with the prober's own 5s dial timeout (`prober.go:45`) |
|
||||||
| `spec.idle.autoStopEnabled` | Auto-stop empty servers | **[INERT]** — idle auto-stop unimplemented (§11) |
|
| `spec.idle.autoStopEnabled` | Auto-stop empty servers | Read at `reconciler.go:175` — but gated on `spec.rcon.enabled`, since the player tally comes from the RCON probe (§11) |
|
||||||
| `spec.idle.emptySecondsBeforeStop` | Empty grace period | **[INERT]** |
|
| `spec.idle.emptySecondsBeforeStop` | Empty grace period | Same branch. Must be `> 0`; the guard treats `0` as "off", not "stop immediately" |
|
||||||
| `spec.storage.retainOnDelete` | Keep/drop PVC on delete | **[INERT]** — world PVCs **always** survive server deletion; only the reaper ever deletes a world PVC (§13) |
|
|
||||||
|
|
||||||
If a runbook tells someone to "tune `startup.timeoutSeconds`" for a hung start,
|
Both startup budgets are measured from the same `status.startRequestedAt`, so
|
||||||
it is wrong — use `kubectl describe pod` (§1a) instead.
|
`readinessTimeoutSeconds` is not a budget *after* pod readiness — it is a
|
||||||
|
deadline for the whole start, applied on the RCON-probe branch.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -503,16 +532,29 @@ This is expected. The world PVC is a StatefulSet `VolumeClaimTemplate`. There is
|
|||||||
**no `persistentVolumeClaimRetentionPolicy` and no finalizer** anywhere in the
|
**no `persistentVolumeClaimRetentionPolicy` and no finalizer** anywhere in the
|
||||||
operator. Deleting the `MinecraftServer` garbage-collects the StatefulSet, but
|
operator. Deleting the `MinecraftServer` garbage-collects the StatefulSet, but
|
||||||
StatefulSet deletion does **not** cascade to its template PVCs, and nothing else
|
StatefulSet deletion does **not** cascade to its template PVCs, and nothing else
|
||||||
cleans them up. So the world PVC **always survives** server deletion, regardless
|
cleans them up. So the world PVC **always survives** server deletion. The
|
||||||
of `spec.storage.retainOnDelete` ([INERT], §12). The **only** code that deletes a
|
**only** code that deletes a world PVC is the reaper, and only after a verified
|
||||||
world PVC is the reaper, and only after a verified backup (§10). To reclaim a
|
backup (§10). To reclaim a world PVC manually:
|
||||||
world PVC manually:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
kubectl get pvc -l app.kubernetes.io/name=<name>
|
kubectl get pvc -l app.kubernetes.io/name=<name>
|
||||||
kubectl delete pvc <pvc> # irreversible — the world is gone
|
kubectl delete pvc <pvc> # irreversible — the world is gone
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`spec.storage.retainOnDelete` sat in the CRD and reached no controller. Spec
|
||||||
|
v4.1 §5 asks for it — 「删除:finalizer 清 Service/STS/ConfigMap,PVC 按
|
||||||
|
`retainOnDelete`」 — and neither half was ever built: there is no finalizer, and
|
||||||
|
nothing read the field. It was removed rather than implemented, which is a
|
||||||
|
deliberate departure from that line, recorded here because the spec is a frozen
|
||||||
|
document and still says otherwise.
|
||||||
|
|
||||||
|
The reasoning is that implementing it buys a second path that deletes a world —
|
||||||
|
one that skips the reaper's verified-backup check — in order to restore a
|
||||||
|
finalizer whose other listed duties (Service, StatefulSet, ConfigMap)
|
||||||
|
ownerReference GC already performs. A CR still carrying the field keeps working:
|
||||||
|
the API server prunes the unknown key on its next write, and nothing above
|
||||||
|
changes, because retention was never conditional in the first place.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14. Metrics for diagnosis (spec §23)
|
## 14. Metrics for diagnosis (spec §23)
|
||||||
|
|||||||
@@ -192,8 +192,6 @@ type StorageSpec struct {
|
|||||||
Size string `json:"size,omitempty"`
|
Size string `json:"size,omitempty"`
|
||||||
// StorageClassName selects the StorageClass; empty uses the default.
|
// StorageClassName selects the StorageClass; empty uses the default.
|
||||||
StorageClassName string `json:"storageClassName,omitempty"`
|
StorageClassName string `json:"storageClassName,omitempty"`
|
||||||
// RetainOnDelete keeps the PVC when the MinecraftServer is deleted.
|
|
||||||
RetainOnDelete bool `json:"retainOnDelete,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LifecycleSpec tunes graceful shutdown (spec §7). The operator injects a
|
// LifecycleSpec tunes graceful shutdown (spec §7). The operator injects a
|
||||||
|
|||||||
@@ -118,9 +118,12 @@ type RegistryConfig struct {
|
|||||||
// modpack's Kaniko build context is pinned. It belongs to the §16 build
|
// modpack's Kaniko build context is pinned. It belongs to the §16 build
|
||||||
// subsystem's input domain (the build-context store), introduced by the
|
// subsystem's input domain (the build-context store), introduced by the
|
||||||
// user-directed modpack approval lane (see internal/submit package doc). The
|
// user-directed modpack approval lane (see internal/submit package doc). The
|
||||||
// lane derives {UserUploadsContext}/{submissionID}/context.tar.gz; the upload
|
// lane derives {UserUploadsContext}/{submissionID}/context.tar.gz; both transports
|
||||||
// transport that places the blob there is a separate, deferred integration
|
// that place the blob there now ship (submit.LocalContextStore for a local path,
|
||||||
// (INTEGRATION-ONLY). It is kept distinct from [archive] on purpose — a world
|
// submit.S3ContextStore for an s3:// base, selected in cmd/felis by the shape of
|
||||||
|
// this value). What stays deferred is the far end — Kaniko reading that context
|
||||||
|
// from inside the build Pod (INTEGRATION-ONLY, see submit/blobstore.go). It is
|
||||||
|
// kept distinct from [archive] on purpose — a world
|
||||||
// archive (§19 WorldArchiver) and a build context (§16) are different artifacts
|
// archive (§19 WorldArchiver) and a build context (§16) are different artifacts
|
||||||
// with different lifecycles, so the two must not share a store binding.
|
// with different lifecycles, so the two must not share a store binding.
|
||||||
UserUploadsContext string `toml:"user_uploads_context"`
|
UserUploadsContext string `toml:"user_uploads_context"`
|
||||||
|
|||||||
+11
-7
@@ -41,11 +41,15 @@
|
|||||||
// read and a CLI read agree instead of the image masquerading as a prerelease. The
|
// read and a CLI read agree instead of the image masquerading as a prerelease. The
|
||||||
// CLI seam (execRunner) is wired for real; only its exec I/O is un-verified here.
|
// CLI seam (execRunner) is wired for real; only its exec I/O is un-verified here.
|
||||||
//
|
//
|
||||||
// - REMAINING INTEGRATION (genuinely I/O-bound — needs a node/cluster/mailbox): the
|
// - REMAINING INTEGRATION (genuinely I/O-bound — needs a cluster/mailbox): the two
|
||||||
// two current-version PRODUCING seams the gatherer still lacks — the k8s read of the
|
// current-version PRODUCING seams NewSysGatherer still leaves nil — the k8s read of
|
||||||
// control-plane Deployment's image (felis-api) and the off-cluster Velocity jar
|
// the control-plane Deployment's image (felis-api) and the Velocity jar inspection —
|
||||||
// inspection, both left nil so those components surface a gather error rather than a
|
// so an IN-CLUSTER caller surfaces a gather error rather than a wrong version. The
|
||||||
// wrong version — plus the concrete Notifier (SMTP + in-game) and Applier
|
// ON-HOST caller has both: NewHostGatherer (gatherer_host.go) answers felis-api from
|
||||||
// (control-plane image bump, cloudflared swap), the `felis update` CLI + CronJob
|
// the running binary's build stamp and Velocity from the installed jar's manifest,
|
||||||
// entry point, and the runtime append of the live Pinned Minecraft fleet.
|
// and that is what the built `felis update` CLI runs on. Still absent: the concrete
|
||||||
|
// Notifier (SMTP + in-game) and Applier (control-plane image bump, cloudflared swap)
|
||||||
|
// — the CLI passes nil for both on purpose, so it reports and never applies — the
|
||||||
|
// in-cluster CronJob entry point, and the runtime append of the live Pinned
|
||||||
|
// Minecraft fleet.
|
||||||
package updater
|
package updater
|
||||||
+30
-15
@@ -1,12 +1,16 @@
|
|||||||
# Felis server-side plugins
|
# Felis server-side plugins
|
||||||
|
|
||||||
These are the in-cluster and edge plugins for Felis. Every module **except the
|
These are the in-cluster and edge plugins for Felis. The Velocity proxy and the three loader
|
||||||
lobby** ships the in-game first leg of the §10 account-link flow: a player who is already online
|
mods ship the in-game first leg of the §10 account-link flow: a player who is already online
|
||||||
(so Mojang has verified their UUID) runs `/link`; the plugin asks felis-api to
|
(so Mojang has verified their UUID) runs `/link`; the plugin asks felis-api to
|
||||||
mint a one-time code for that UUID and shows it in chat. The player then enters
|
mint a one-time code for that UUID and shows it in chat. The player then enters
|
||||||
the code on the web console → **Account** page (the second leg), which binds the
|
the code on the web console → **Account** page (the second leg), which binds the
|
||||||
code to their logged-in account. The web side is already built.
|
code to their logged-in account. The web side is already built.
|
||||||
|
|
||||||
|
The **limbo** module reaches the same felis-api endpoint without a command: it is the login
|
||||||
|
gate, so it mints the code on join for anyone not yet linked and holds them until they redeem
|
||||||
|
it. The **paper** lobby ships neither — see below.
|
||||||
|
|
||||||
The **Velocity** module additionally carries the §11 domain-autostart routing
|
The **Velocity** module additionally carries the §11 domain-autostart routing
|
||||||
loop — recognizing each server's subdomain, registering backends dynamically,
|
loop — recognizing each server's subdomain, registering backends dynamically,
|
||||||
waking a sleeping target and holding the player until it is ready. It is a full
|
waking a sleeping target and holding the player until it is ready. It is a full
|
||||||
@@ -19,14 +23,22 @@ The **Paper** module is different in kind: it is the §12 lobby UI face. It ship
|
|||||||
to Velocity, which is the only side that ever talks to felis-api. See
|
to Velocity, which is the only side that ever talks to felis-api. See
|
||||||
**[Lobby menu](#lobby-menu-§12)** below.
|
**[Lobby menu](#lobby-menu-§12)** below.
|
||||||
|
|
||||||
| Module | Platform | Target | Jar |
|
| Module | Platform | Target | Jar | Built by the installer |
|
||||||
| ------------------ | --------------------------- | ----------------------------------- | ---------------------------- |
|
| ------------------ | --------------------------- | ----------------------------------- | ---------------------------- | ---------------------- |
|
||||||
| `velocity/` | Velocity proxy plugin | velocity-api 3.3.0-SNAPSHOT | `felis-velocity-0.2.0.jar` |
|
| `velocity/` | Velocity proxy plugin | velocity-api 3.3.0-SNAPSHOT | `felis-velocity-0.1.0.jar` | yes |
|
||||||
| `fabric/` | Fabric server mod | MC 1.20.1 / fabric-loader 0.16.x | `felis-fabric-0.1.0.jar` |
|
| `limbo/` | LOOHP/Limbo plugin (login) | Limbo API / Java 17 bytecode | `felis-limbo-0.1.0.jar` | yes |
|
||||||
| `forge/` | Forge server mod | MC 1.20.1 / Forge 47.3.0 | `felis-forge-0.1.0.jar` |
|
| `paper/` | Paper server plugin (lobby) | paper-api 1.21.4-R0.1-SNAPSHOT | `felis-paper-0.1.0.jar` | yes |
|
||||||
| `neoforge/` | NeoForge server mod | MC 1.20.4 / NeoForge 20.4.251 | `felis-neoforge-0.1.0.jar` |
|
| `fabric/` | Fabric server mod | MC 1.20.1 / fabric-loader 0.16.x | `felis-fabric-0.1.0.jar` | no |
|
||||||
| `paper/` | Paper server plugin (lobby) | paper-api 1.21.4-R0.1-SNAPSHOT | `felis-paper-0.1.0.jar` |
|
| `forge/` | Forge server mod | MC 1.20.1 / Forge 47.3.0 | `felis-forge-0.1.0.jar` | no |
|
||||||
| `shared/` | *(not built on its own)* | — | source compiled into each |
|
| `neoforge/` | NeoForge server mod | MC 1.20.4 / NeoForge 20.4.251 | `felis-neoforge-0.1.0.jar` | no |
|
||||||
|
| `shared/` | *(not built on its own)* | — | source compiled into each | source only |
|
||||||
|
|
||||||
|
"Built by the installer" is what `deploy/bootstrap.sh` produces, and it is the same set
|
||||||
|
`bootstrap_asset.go` embeds into the felis binary for the TUI install path, which has no source
|
||||||
|
checkout to build from. **The three loader mods are not in that set** — a finished install has
|
||||||
|
no `felis-fabric`/`felis-forge`/`felis-neoforge` jar anywhere. They build from this checkout with
|
||||||
|
the commands under [Building](#building) and are deployed by hand; the account-link flow they
|
||||||
|
carry works, but nothing installs them for you.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -176,6 +188,7 @@ preference):
|
|||||||
| Module | Gradle | Why |
|
| Module | Gradle | Why |
|
||||||
| ----------- | ----------- | --------------------------------------------------------------- |
|
| ----------- | ----------- | --------------------------------------------------------------- |
|
||||||
| `velocity` | 9.5.1 (system) | plain `java` plugin — no loader Gradle plugin |
|
| `velocity` | 9.5.1 (system) | plain `java` plugin — no loader Gradle plugin |
|
||||||
|
| `limbo` | 9.5.1 (system), **JDK 21 toolchain** | plain `java` plugin; current LOOHP/Limbo releases ship class-file major 65, so the compiler JDK must be ≥ 21 to read them. It emits `release 17` bytecode, so the jar still loads on any Limbo running Java 17+ |
|
||||||
| `fabric` | 8.8 (wrapper) | loom 1.7.4 uses `Problems.forNamespace`, removed in Gradle 9 |
|
| `fabric` | 8.8 (wrapper) | loom 1.7.4 uses `Problems.forNamespace`, removed in Gradle 9 |
|
||||||
| `forge` | 8.8 (wrapper) | ForgeGradle 6 is Gradle-8-only |
|
| `forge` | 8.8 (wrapper) | ForgeGradle 6 is Gradle-8-only |
|
||||||
| `neoforge` | 8.14 (wrapper) | NeoGradle 7.1.38 requires Gradle API ≥ 8.14 |
|
| `neoforge` | 8.14 (wrapper) | NeoGradle 7.1.38 requires Gradle API ≥ 8.14 |
|
||||||
@@ -185,18 +198,20 @@ preference):
|
|||||||
# Velocity — system Gradle is fine
|
# Velocity — system Gradle is fine
|
||||||
gradle -p plugins/velocity build
|
gradle -p plugins/velocity build
|
||||||
|
|
||||||
# Paper — system Gradle too, but it compiles on a Java-21 toolchain (see table)
|
# Paper and limbo — system Gradle too, but both compile on a Java-21 toolchain (see table)
|
||||||
gradle -p plugins/paper build
|
gradle -p plugins/paper build
|
||||||
|
gradle -p plugins/limbo build
|
||||||
|
|
||||||
# Fabric / Forge / NeoForge — use the per-module wrapper
|
# Fabric / Forge / NeoForge — use the per-module wrapper. Nothing installs these; the jar you
|
||||||
|
# want is the one this produces.
|
||||||
plugins/fabric/gradlew -p plugins/fabric build
|
plugins/fabric/gradlew -p plugins/fabric build
|
||||||
plugins/forge/gradlew -p plugins/forge build
|
plugins/forge/gradlew -p plugins/forge build
|
||||||
plugins/neoforge/gradlew -p plugins/neoforge build
|
plugins/neoforge/gradlew -p plugins/neoforge build
|
||||||
```
|
```
|
||||||
|
|
||||||
Requires JDK 17 — **except `paper`, which needs a Java-21 toolchain available to
|
Requires JDK 17 — **except `paper` and `limbo`, which need a Java-21 toolchain available to
|
||||||
Gradle** (paper-api 1.21.4 is a Java-21 artifact; the rest of the suite is Java
|
Gradle** (paper-api 1.21.4 is a Java-21 artifact and the Limbo API is compiled to major 65; the
|
||||||
17). The first build of each mod downloads and remaps/decompiles Minecraft, so it
|
rest of the suite is Java 17). The first build of each mod downloads and remaps/decompiles Minecraft, so it
|
||||||
takes a few minutes; subsequent builds are fast. Jars land in each module's
|
takes a few minutes; subsequent builds are fast. Jars land in each module's
|
||||||
`build/libs/`.
|
`build/libs/`.
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user