feat(bootstrap): verify Paper and Velocity jars against Fill's digest

This commit is contained in:
flyemoji committed 2026-08-04 17:36:47 +09:00
1 parent 3f2b28d0ec
commit d9246ddae6
7 files changed
+168 -9

No files matched your search

+31
View File
@@ -66,6 +66,37 @@ func TestLobbyLuckPermsWiringIsConsistent(t *testing.T) {
} }
} }
// The Paper jar digest rides the same cross-file contract as LuckPerms above: bootstrap.sh
// resolves "url sha256" out of Fill's content-addressed download URL and passes the digest
// as a build-arg the Dockerfile must require and verify. docker only WARNS about an unknown
// --build-arg, so a renamed arg would surface as a required-arg failure on a real host
// mid-install — this test is the only compile step the pairing gets.
//
// Both images pull the same jar from the same URL, so both have to check it: a gate on one
// of them leaves the other booting on whatever bytes happened to arrive.
func TestPaperJarDigestWiringIsConsistent(t *testing.T) {
const arg = "PAPER_JAR_SHA256"
if n := strings.Count(BootstrapScript(), "--build-arg "+arg+"="); n < 2 {
t.Errorf("bootstrap.sh passes --build-arg %s %d time(s); the lobby and the "+
"plain-Paper build each need it", arg, n)
}
for _, name := range []string{"deploy/lobby/Dockerfile", "deploy/paper/Dockerfile"} {
dockerfile := readGameStackFile(t, name)
if !strings.Contains(dockerfile, "ARG "+arg) {
t.Errorf("%s declares no ARG %s", name, arg)
}
if !strings.Contains(dockerfile, `if [ -z "${PAPER_JAR_SHA256:-}" ]`) {
t.Errorf("%s does not fail the build when %s is unset", name, arg)
}
// Requiring the arg is not the same as spending it, and which file gets hashed
// matters as much as the command: a `sha256sum -c` over some other download
// would satisfy a bare substring check while paper.jar still arrives unchecked.
if !strings.Contains(dockerfile, `echo "$PAPER_JAR_SHA256 /paper/paper.jar" | sha256sum -c`) {
t.Errorf("%s never verifies /paper/paper.jar against %s", name, arg)
}
}
}
// A 1.8 client joining a protocol-47 backend dies on the first chunk unless ViaVersion's // A 1.8 client joining a protocol-47 backend dies on the first chunk unless ViaVersion's
// serverside block-connection tracking is off: under modern forwarding the Velocity injector // serverside block-connection tracking is off: under modern forwarding the Velocity injector
// reports 1.13 as the lowest supported protocol, ConnectionData.init() returns early on that, // reports 1.13 as the lowest supported protocol, ConnectionData.init() returns early on that,
+29 -8
View File
@@ -1200,7 +1200,7 @@ game_stack_source() {
# MC_VERSION is read off Limbo's CI artifact name (Limbo-<limbo-ver>-<mc-ver>.jar), which # MC_VERSION is read off Limbo's CI artifact name (Limbo-<limbo-ver>-<mc-ver>.jar), which
# is the only place the pairing is published. # is the only place the pairing is published.
resolve_game_jars() { resolve_game_jars() {
local ci="https://ci.loohpjames.com/job/Limbo/lastSuccessfulBuild" meta file base rest local ci="https://ci.loohpjames.com/job/Limbo/lastSuccessfulBuild" meta file base rest paper
log "resolving the newest LOOHP/Limbo CI build" log "resolving the newest LOOHP/Limbo CI build"
# Fetch first, filter second: `curl | grep | head` dies of SIGPIPE under `set -o pipefail` # Fetch first, filter second: `curl | grep | head` dies of SIGPIPE under `set -o pipefail`
# the moment head closes the pipe early. Same shape everywhere below. # the moment head closes the pipe early. Same shape everywhere below.
@@ -1221,8 +1221,10 @@ resolve_game_jars() {
# PaperMC Fill v3. The old api.papermc.io v2 has returned HTTP 410 since 2026-07-01 and # PaperMC Fill v3. The old api.papermc.io v2 has returned HTTP 410 since 2026-07-01 and
# is never coming back; Fill wants a descriptive User-Agent. # is never coming back; Fill wants a descriptive User-Agent.
log "resolving the newest Paper ${MC_VERSION} build" log "resolving the newest Paper ${MC_VERSION} build"
PAPER_JAR_URL="$(papermc_latest_jar paper "$MC_VERSION")" \ paper="$(papermc_latest_jar paper "$MC_VERSION")" \
|| die "could not resolve a Paper build for Minecraft ${MC_VERSION} (the login gate pins this protocol; the build likely exists — Fill upstream is down or flapping)" || die "could not resolve a Paper build for Minecraft ${MC_VERSION} (the login gate pins this protocol; the build likely exists — Fill upstream is down, flapping, or no longer content-addressed)"
PAPER_JAR_URL="${paper% *}"
PAPER_JAR_SHA256="${paper##* }"
# LuckPerms is not version-matched to MC_VERSION the way Paper is: it ships one # LuckPerms is not version-matched to MC_VERSION the way Paper is: it ships one
# current Bukkit build that supports the whole supported Minecraft range, so there is # current Bukkit build that supports the whole supported Minecraft range, so there is
# no per-version endpoint to ask. # no per-version endpoint to ask.
@@ -1250,20 +1252,29 @@ luckperms_latest_jar() {
printf '%s\n' "$url" printf '%s\n' "$url"
} }
# papermc_latest_jar prints the download URL of the newest build of <project> <version>. # papermc_latest_jar prints "<url> <sha256>" for the newest build of <project> <version>.
# --retry rides out Fill's transient gateway errors (502/503/504 are in curl's retry # --retry rides out Fill's transient gateway errors (502/503/504 are in curl's retry
# set): a single blip must not abort the whole bootstrap claiming the build is missing. # set): a single blip must not abort the whole bootstrap claiming the build is missing.
# Plain --retry only, deliberately: --retry-connrefused needs curl 7.52+, which the yum # Plain --retry only, deliberately: --retry-connrefused needs curl 7.52+, which the yum
# (el7) path does not have, and it would only add ECONNREFUSED to an already-covered set. # (el7) path does not have, and it would only add ECONNREFUSED to an already-covered set.
# The digest is not fished out of the JSON separately: Fill's download URLs are
# content-addressed (/v1/objects/<sha256>/<name>.jar), so the path segment names the
# bytes the URL serves and both halves come from the same grep of the same response. A
# URL without that shape fails the resolve rather than waving the download through
# unchecked.
papermc_latest_jar() { papermc_latest_jar() {
local project="$1" version="$2" json urls url local project="$1" version="$2" json urls url sha
json="$(curl -fsSL --retry 5 --retry-delay 2 \ json="$(curl -fsSL --retry 5 --retry-delay 2 \
-A "felis-bootstrap (+https://github.com/MliroLirrorsIngenuity/Felis)" \ -A "felis-bootstrap (+https://github.com/MliroLirrorsIngenuity/Felis)" \
"https://fill.papermc.io/v3/projects/${project}/versions/${version}/builds/latest")" || return 1 "https://fill.papermc.io/v3/projects/${project}/versions/${version}/builds/latest")" || return 1
urls="$(printf '%s' "$json" | grep -o 'https://fill-data\.papermc\.io/[^"]*\.jar' || true)" urls="$(printf '%s' "$json" | grep -o 'https://fill-data\.papermc\.io/[^"]*\.jar' || true)"
url="${urls%%$'\n'*}" url="${urls%%$'\n'*}"
[ -n "$url" ] || return 1 [ -n "$url" ] || return 1
printf '%s\n' "$url" sha="${url#*/objects/}"
sha="${sha%%/*}"
case "$sha" in *[!0-9a-f]*|"") return 1 ;; esac
[ "${#sha}" -eq 64 ] || return 1
printf '%s %s\n' "$url" "$sha"
} }
build_game_stack() { build_game_stack() {
@@ -1281,6 +1292,7 @@ build_game_stack() {
log "building ${FELIS_LOBBY_IMAGE} (Paper ${MC_VERSION} + felis-paper /menu + LuckPerms)" log "building ${FELIS_LOBBY_IMAGE} (Paper ${MC_VERSION} + felis-paper /menu + LuckPerms)"
docker build -f "${GAME_STACK_DIR}/deploy/lobby/Dockerfile" \ docker build -f "${GAME_STACK_DIR}/deploy/lobby/Dockerfile" \
--build-arg PAPER_JAR_URL="$PAPER_JAR_URL" \ --build-arg PAPER_JAR_URL="$PAPER_JAR_URL" \
--build-arg PAPER_JAR_SHA256="$PAPER_JAR_SHA256" \
--build-arg LUCKPERMS_JAR_URL="$LUCKPERMS_JAR_URL" \ --build-arg LUCKPERMS_JAR_URL="$LUCKPERMS_JAR_URL" \
-t "$FELIS_LOBBY_IMAGE" "$GAME_STACK_DIR" -t "$FELIS_LOBBY_IMAGE" "$GAME_STACK_DIR"
@@ -1289,6 +1301,7 @@ build_game_stack() {
log "building ${FELIS_PAPER_IMAGE} (plain Paper ${MC_VERSION}, forwarding via the operator initContainer)" log "building ${FELIS_PAPER_IMAGE} (plain Paper ${MC_VERSION}, forwarding via the operator initContainer)"
docker build -f "${GAME_STACK_DIR}/deploy/paper/Dockerfile" \ docker build -f "${GAME_STACK_DIR}/deploy/paper/Dockerfile" \
--build-arg PAPER_JAR_URL="$PAPER_JAR_URL" \ --build-arg PAPER_JAR_URL="$PAPER_JAR_URL" \
--build-arg PAPER_JAR_SHA256="$PAPER_JAR_SHA256" \
-t "$FELIS_PAPER_IMAGE" "$GAME_STACK_DIR" -t "$FELIS_PAPER_IMAGE" "$GAME_STACK_DIR"
local img local img
@@ -1486,7 +1499,7 @@ install_jre() {
install_velocity() { install_velocity() {
install_jre install_jre
local url tmp have want local url tmp have want resolved
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" ] \
@@ -1514,12 +1527,20 @@ install_velocity() {
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"
url="$(papermc_latest_jar velocity "$FELIS_VELOCITY_VERSION")" \ resolved="$(papermc_latest_jar velocity "$FELIS_VELOCITY_VERSION")" \
|| die "no Velocity build for ${FELIS_VELOCITY_VERSION} (override with FELIS_VELOCITY_VERSION)" || die "no Velocity build for ${FELIS_VELOCITY_VERSION} (override with FELIS_VELOCITY_VERSION)"
url="${resolved% *}"
want="${resolved##* }"
log "downloading Velocity ${FELIS_VELOCITY_VERSION}" log "downloading Velocity ${FELIS_VELOCITY_VERSION}"
tmp="$(mktemp "${VELOCITY_DIR}/.velocity.jar.XXXXXX")" tmp="$(mktemp "${VELOCITY_DIR}/.velocity.jar.XXXXXX")"
remember_temp "$tmp" remember_temp "$tmp"
curl -fsSL "$url" -o "$tmp" || die "failed to download Velocity: ${url}" curl -fsSL "$url" -o "$tmp" || die "failed to download Velocity: ${url}"
# The same gate the Via plugins and the fork jar pass: this jar is the proxy every
# player connects through, and Fill already promised its digest in the URL — a
# truncated or tampered download becomes a refusal here, not a proxy that won't boot.
have="$(sha256sum <"$tmp" | cut -d' ' -f1)"
[ "$have" = "$want" ] \
|| die "Velocity ${FELIS_VELOCITY_VERSION} checksum mismatch: got ${have}, expected ${want}"
atomic_install_file "$tmp" "${VELOCITY_DIR}/velocity.jar" 0644 root root atomic_install_file "$tmp" "${VELOCITY_DIR}/velocity.jar" 0644 root root
fi fi
+71
View File
@@ -61,6 +61,77 @@ expect "an uppercase digest is the same digest" "LOG: installing the Felis-Legac
out="$(run_gate "$(printf '%s' "$want" | sed 's/../& /g')")" 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" expect "a space-separated digest is the same digest" "LOG: installing the Felis-Legacy Velocity fork" "$out"
# --- papermc_latest_jar answers "url sha256" from one response --------------------------
# Fill's download URLs are content-addressed (/v1/objects/<sha256>/<name>.jar), and the
# resolver's contract is to hand both halves back from the same grep — or refuse a URL
# that carries no digest, rather than wave the download through unchecked. Run under
# bash, not sh: bootstrap.sh is bash and the function uses $'\n'.
fn="$(awk '/^papermc_latest_jar\(\)/,/^}/' "$BS")"
[ -n "$fn" ] || { echo "FAIL: no papermc_latest_jar in $BS"; exit 1; }
[ "$(printf '%s\n' "$fn" | wc -l)" -lt 30 ] \
|| { echo "FAIL: the extracted papermc_latest_jar is not just the function -- did its closing brace move?"; exit 1; }
rsha=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
run_resolver() { # canned-fill-response
CANNED="$1" bash -c '
curl() { printf "%s" "$CANNED"; }
'"$fn"'
if out="$(papermc_latest_jar velocity 3.5.1)"; then
printf "RESOLVED %s\n" "$out"
else
printf "REFUSED\n"
fi
'
}
out="$(run_resolver "{\"url\":\"https://fill-data.papermc.io/v1/objects/${rsha}/velocity-3.5.1-615.jar\"}")"
expect "the resolver pairs the url with its own digest" \
"RESOLVED https://fill-data.papermc.io/v1/objects/${rsha}/velocity-3.5.1-615.jar ${rsha}" "$out"
out="$(run_resolver '{"url":"https://fill-data.papermc.io/mirror/velocity-3.5.1-615.jar"}')"
expect "a URL that carries no digest is refused" "REFUSED" "$out"
# --- the resolved-Velocity digest gate --------------------------------------------------
# The download must hash to what the content-addressed URL promised, BEFORE
# atomic_install_file — the same refusal the Via plugins and the fork jar already get.
# The end pattern spells ${VELOCITY_DIR} with dots: escaped braces are literal in gawk
# and mawk but undefined in POSIX awk, and CI's awk is whatever ubuntu ships.
vblock="$(awk '/log "resolving the newest Velocity/,/atomic_install_file "\$tmp" "\$.VELOCITY_DIR.\/velocity\.jar"/' "$BS")"
[ -n "$vblock" ] || { echo "FAIL: no resolved-Velocity install block found in $BS"; exit 1; }
[ "$(printf '%s\n' "$vblock" | wc -l)" -lt 30 ] \
|| { echo "FAIL: the extracted block is not the velocity install -- did its last line move?"; exit 1; }
vdir="$(mktemp -d)"
trap 'rm -f "$jar"; rm -rf "$vdir"' EXIT
vwant="$(printf 'stand-in velocity build\n' | sha256sum | cut -d' ' -f1)"
run_velocity_install() { # digest-the-resolver-reports
WANT="$1" VELOCITY_DIR="$vdir" FELIS_VELOCITY_VERSION=3.5.1 bash -c '
die() { printf "DIE: %s\n" "$*"; exit 1; }
log() { printf "LOG: %s\n" "$*"; }
remember_temp() { :; }
papermc_latest_jar() {
printf "%s %s\n" "https://fill-data.papermc.io/v1/objects/${WANT}/velocity-3.5.1-615.jar" "$WANT"
}
curl() { while [ "$#" -gt 1 ] && [ "$1" != "-o" ]; do shift; done; printf "stand-in velocity build\n" > "$2"; }
atomic_install_file() { printf "INSTALL: %s\n" "$2"; }
'"$vblock"
}
out="$(run_velocity_install deadbeef)"
expect "a download that does not hash to the promised digest is refused" \
"DIE: Velocity 3.5.1 checksum mismatch: got ${vwant}, expected deadbeef" "$out"
case "$out" in
*INSTALL:*) echo "FAIL a refused download must not reach atomic_install_file"; fails=$((fails + 1)) ;;
*) echo "PASS a refused download is not installed" ;;
esac
out="$(run_velocity_install "$vwant")"
expect "the matching download installs" "INSTALL: ${vdir}/velocity.jar" "$out"
# --------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------
if [ "$fails" -eq 0 ]; then if [ "$fails" -eq 0 ]; then
echo "ALL PASS" echo "ALL PASS"
+18 -1
View File
@@ -13,7 +13,8 @@
# 1. Prebuilt tars at deploy/images/felis-limbo.tar + felis-lobby.tar (imported as-is). # 1. Prebuilt tars at deploy/images/felis-limbo.tar + felis-lobby.tar (imported as-is).
# 2. Otherwise built on this host with docker, resolving the LOOHP/Limbo CI jar and # 2. Otherwise built on this host with docker, resolving the LOOHP/Limbo CI jar and
# the latest stable Paper jar automatically. Override any of: # the latest stable Paper jar automatically. Override any of:
# LIMBO_JAR_URL LIMBO_SCHEM_URL LIMBO_VERSION PAPER_JAR_URL PAPER_MC_VERSION # LIMBO_JAR_URL LIMBO_SCHEM_URL LIMBO_VERSION PAPER_JAR_URL PAPER_JAR_SHA256
# PAPER_MC_VERSION
# #
# Toggles: SKIP_BOOTSTRAP=1 (base already up), SKIP_SETUP=1 (stop before the TUI). # Toggles: SKIP_BOOTSTRAP=1 (base already up), SKIP_SETUP=1 (stop before the TUI).
set -Eeuo pipefail set -Eeuo pipefail
@@ -73,9 +74,24 @@ else
: "${PAPER_MC_VERSION:=1.21.8}" : "${PAPER_MC_VERSION:=1.21.8}"
: "${PAPER_JAR_URL:=$(curl -fsSL --max-time 30 "https://fill.papermc.io/v3/projects/paper/versions/${PAPER_MC_VERSION}/builds/latest" | grep -oE 'https://fill-data\.papermc\.io/[^"]+\.jar' | head -1)}" : "${PAPER_JAR_URL:=$(curl -fsSL --max-time 30 "https://fill.papermc.io/v3/projects/paper/versions/${PAPER_MC_VERSION}/builds/latest" | grep -oE 'https://fill-data\.papermc\.io/[^"]+\.jar' | head -1)}"
[ -n "$PAPER_JAR_URL" ] || die "could not resolve the Paper jar; set PAPER_JAR_URL" [ -n "$PAPER_JAR_URL" ] || die "could not resolve the Paper jar; set PAPER_JAR_URL"
# Both Dockerfiles require the jar's digest. The fill-data URL is content-addressed
# (the objects/ path segment IS the sha256), so it is derived rather than asked for;
# a mirror override carries no such segment and must bring its own digest.
if [ -z "${PAPER_JAR_SHA256:-}" ]; then
sha="${PAPER_JAR_URL#*/objects/}"
sha="${sha%%/*}"
case "$sha" in
*[!0-9a-f]*|"") sha="" ;;
esac
if [ "${#sha}" -ne 64 ]; then
die "cannot derive the Paper jar sha256 from PAPER_JAR_URL (not a content-addressed fill-data URL); set PAPER_JAR_SHA256"
fi
PAPER_JAR_SHA256="$sha"
fi
log "building $LOBBY_IMAGE (Paper $PAPER_MC_VERSION)" log "building $LOBBY_IMAGE (Paper $PAPER_MC_VERSION)"
docker build -f "$SRC_DIR/deploy/lobby/Dockerfile" \ docker build -f "$SRC_DIR/deploy/lobby/Dockerfile" \
--build-arg PAPER_JAR_URL="$PAPER_JAR_URL" \ --build-arg PAPER_JAR_URL="$PAPER_JAR_URL" \
--build-arg PAPER_JAR_SHA256="$PAPER_JAR_SHA256" \
-t "$LOBBY_IMAGE" "$SRC_DIR" -t "$LOBBY_IMAGE" "$SRC_DIR"
docker save "$LOBBY_IMAGE" | "$K3S" ctr images import - docker save "$LOBBY_IMAGE" | "$K3S" ctr images import -
@@ -84,6 +100,7 @@ else
log "building $PAPER_IMAGE (plain Paper $PAPER_MC_VERSION, forwarding via the operator initContainer)" log "building $PAPER_IMAGE (plain Paper $PAPER_MC_VERSION, forwarding via the operator initContainer)"
docker build -f "$SRC_DIR/deploy/paper/Dockerfile" \ docker build -f "$SRC_DIR/deploy/paper/Dockerfile" \
--build-arg PAPER_JAR_URL="$PAPER_JAR_URL" \ --build-arg PAPER_JAR_URL="$PAPER_JAR_URL" \
--build-arg PAPER_JAR_SHA256="$PAPER_JAR_SHA256" \
-t "$PAPER_IMAGE" "$SRC_DIR" -t "$PAPER_IMAGE" "$SRC_DIR"
docker save "$PAPER_IMAGE" | "$K3S" ctr images import - docker save "$PAPER_IMAGE" | "$K3S" ctr images import -
fi fi
+9
View File
@@ -9,6 +9,7 @@
# Fill v3 API — api.papermc.io v2 has returned HTTP 410 since 2026-07-01): # Fill v3 API — api.papermc.io v2 has returned HTTP 410 since 2026-07-01):
# docker build -f deploy/lobby/Dockerfile \ # docker build -f deploy/lobby/Dockerfile \
# --build-arg PAPER_JAR_URL=https://fill-data.papermc.io/v1/objects/<sha>/paper-26.2-<build>.jar \ # --build-arg PAPER_JAR_URL=https://fill-data.papermc.io/v1/objects/<sha>/paper-26.2-<build>.jar \
# --build-arg PAPER_JAR_SHA256=<that same sha — the objects/ path segment> \
# --build-arg LUCKPERMS_JAR_URL="$(curl -fsSL https://metadata.luckperms.net/data/all \ # --build-arg LUCKPERMS_JAR_URL="$(curl -fsSL https://metadata.luckperms.net/data/all \
# | grep -o 'https://download.luckperms.net/[^"]*/bukkit/loader/[^"]*\.jar')" \ # | grep -o 'https://download.luckperms.net/[^"]*/bukkit/loader/[^"]*\.jar')" \
# -t felis-lobby:demo . # -t felis-lobby:demo .
@@ -42,6 +43,10 @@ RUN cd plugins/paper \
# also runs the plugin's Java-21 bytecode, so only the runtime moves. # also runs the plugin's Java-21 bytecode, so only the runtime moves.
FROM eclipse-temurin:25-jre FROM eclipse-temurin:25-jre
ARG PAPER_JAR_URL ARG PAPER_JAR_URL
# Required alongside the URL: Fill's URLs are content-addressed, but nothing enforces
# that shape at build time. Checking the digest after the download turns a truncated or
# tampered fetch into a failed build instead of a lobby booted on the wrong bytes.
ARG PAPER_JAR_SHA256
# LuckPerms is required, not optional: the panel's whole permission surface # LuckPerms is required, not optional: the panel's whole permission surface
# (internal/api/handlers_access.go) issues `lp user ...` over RCON, so a lobby built # (internal/api/handlers_access.go) issues `lp user ...` over RCON, so a lobby built
# without it answers every grant with "Unknown command" — a failure the operator only # without it answers every grant with "Unknown command" — a failure the operator only
@@ -55,12 +60,16 @@ RUN set -eu; \
if [ -z "${PAPER_JAR_URL:-}" ]; then \ if [ -z "${PAPER_JAR_URL:-}" ]; then \
echo "ERROR: --build-arg PAPER_JAR_URL=<paper jar> is required" >&2; exit 1; \ echo "ERROR: --build-arg PAPER_JAR_URL=<paper jar> is required" >&2; exit 1; \
fi; \ fi; \
if [ -z "${PAPER_JAR_SHA256:-}" ]; then \
echo "ERROR: --build-arg PAPER_JAR_SHA256=<paper jar sha256> is required" >&2; exit 1; \
fi; \
if [ -z "${LUCKPERMS_JAR_URL:-}" ]; then \ if [ -z "${LUCKPERMS_JAR_URL:-}" ]; then \
echo "ERROR: --build-arg LUCKPERMS_JAR_URL=<luckperms bukkit jar> is required" >&2; exit 1; \ echo "ERROR: --build-arg LUCKPERMS_JAR_URL=<luckperms bukkit jar> is required" >&2; exit 1; \
fi; \ fi; \
apt-get update && apt-get install -y --no-install-recommends curl ca-certificates; \ apt-get update && apt-get install -y --no-install-recommends curl ca-certificates; \
mkdir -p /paper/plugins; \ mkdir -p /paper/plugins; \
curl -fSL "$PAPER_JAR_URL" -o /paper/paper.jar; \ curl -fSL "$PAPER_JAR_URL" -o /paper/paper.jar; \
echo "$PAPER_JAR_SHA256 /paper/paper.jar" | sha256sum -c; \
curl -fSL "$LUCKPERMS_JAR_URL" -o /paper/plugins/LuckPerms.jar; \ curl -fSL "$LUCKPERMS_JAR_URL" -o /paper/plugins/LuckPerms.jar; \
apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*; \ apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*; \
echo "eula=true" > /paper/eula.txt echo "eula=true" > /paper/eula.txt
+1
View File
@@ -29,6 +29,7 @@ this at every layer:
``` ```
docker build -f deploy/lobby/Dockerfile \ docker build -f deploy/lobby/Dockerfile \
--build-arg PAPER_JAR_URL=https://<mirror>/paper-1.21.x-<build>.jar \ --build-arg PAPER_JAR_URL=https://<mirror>/paper-1.21.x-<build>.jar \
--build-arg PAPER_JAR_SHA256=<sha256 of that jar> \
-t felis-lobby:demo . -t felis-lobby:demo .
docker save felis-lobby:demo | sudo k3s ctr images import - docker save felis-lobby:demo | sudo k3s ctr images import -
# felis.toml → [velocity] lobby_image = "felis-lobby:demo" # felis.toml → [velocity] lobby_image = "felis-lobby:demo"
+9
View File
@@ -18,6 +18,7 @@
# API — the SAME url the lobby build resolves, so this reuses it and adds no new dependency): # API — the SAME url the lobby build resolves, so this reuses it and adds no new dependency):
# docker build -f deploy/paper/Dockerfile \ # docker build -f deploy/paper/Dockerfile \
# --build-arg PAPER_JAR_URL=https://fill-data.papermc.io/v1/objects/<sha>/paper-<ver>-<build>.jar \ # --build-arg PAPER_JAR_URL=https://fill-data.papermc.io/v1/objects/<sha>/paper-<ver>-<build>.jar \
# --build-arg PAPER_JAR_SHA256=<that same sha — the objects/ path segment> \
# -t felis-paper:demo . # -t felis-paper:demo .
# docker save felis-paper:demo | sudo k3s ctr images import - # docker save felis-paper:demo | sudo k3s ctr images import -
# # felis.toml → recommended via 0019_recommended_paper.sql (no [velocity] key points here) # # felis.toml → recommended via 0019_recommended_paper.sql (no [velocity] key points here)
@@ -30,13 +31,21 @@
# to boot on anything older. # to boot on anything older.
FROM eclipse-temurin:25-jre FROM eclipse-temurin:25-jre
ARG PAPER_JAR_URL ARG PAPER_JAR_URL
# Required alongside the URL: Fill's URLs are content-addressed, but nothing enforces
# that shape at build time. Checking the digest after the download turns a truncated or
# tampered fetch into a failed build instead of a server booted on the wrong bytes.
ARG PAPER_JAR_SHA256
RUN set -eu; \ RUN set -eu; \
if [ -z "${PAPER_JAR_URL:-}" ]; then \ if [ -z "${PAPER_JAR_URL:-}" ]; then \
echo "ERROR: --build-arg PAPER_JAR_URL=<paper jar> is required" >&2; exit 1; \ echo "ERROR: --build-arg PAPER_JAR_URL=<paper jar> is required" >&2; exit 1; \
fi; \ fi; \
if [ -z "${PAPER_JAR_SHA256:-}" ]; then \
echo "ERROR: --build-arg PAPER_JAR_SHA256=<paper jar sha256> is required" >&2; exit 1; \
fi; \
apt-get update && apt-get install -y --no-install-recommends curl ca-certificates; \ apt-get update && apt-get install -y --no-install-recommends curl ca-certificates; \
mkdir -p /paper; \ mkdir -p /paper; \
curl -fSL "$PAPER_JAR_URL" -o /paper/paper.jar; \ curl -fSL "$PAPER_JAR_URL" -o /paper/paper.jar; \
echo "$PAPER_JAR_SHA256 /paper/paper.jar" | sha256sum -c; \
apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
COPY deploy/paper/entrypoint.sh /usr/local/bin/felis-entrypoint.sh COPY deploy/paper/entrypoint.sh /usr/local/bin/felis-entrypoint.sh