fix(images): 服务器镜像在创建时固定到仓库 digest,更换镜像需确认备份,安装器重建前先固定旧服并推送不可变版本标签

This commit is contained in:
Lemon-miaow committed 2026-09-24 16:57:38 +08:00
1 parent 857b6da886
commit 215bfd78d7
29 files changed
+1149 -29

No files matched your search

+45
View File
@@ -1007,6 +1007,27 @@ pin_registry_images() {
done
}
# pin_user_server_images fixes every user server still naming a tag in the platform
# registry (felis/paper:demo) to the digest that tag names now. It has to run before
# build_game_stack and push_images_to_registry put new builds under those tags: a
# server left on the bare tag would boot the new build on its next wake and open its
# world with a newer Minecraft version, and chunk upgrades cannot be undone. felis-api
# pins every server it creates; this catches the ones created before it did. A fresh
# install has no CRD, so nothing to pin; a running server restarts once onto the
# build it already runs.
pin_user_server_images() {
kube get crd minecraftservers.felis.lolicon.best >/dev/null 2>&1 || return 0
# The registry answers the lookups, and a k3s restart above may have left its pod
# still starting. A registry that never comes up fails the lookups below, loudly.
kube -n "$CONTROL_NS" rollout status deployment/registry --timeout=180s >/dev/null 2>&1 || true
if "$HOST_BIN" pin-images --namespace "$MINECRAFT_NS" \
--registry "$REGISTRY_URL" --endpoint "$REGISTRY_PUSH_HOST"; then
ok "user servers pinned to the builds they run"
else
warn "could not pin every user server listed above to its current build: each one still names a tag this run is about to point at a new build, so its next start may open its world with a newer Minecraft version. Pin it before starting it again: sudo felis pin-images (docs/troubleshooting.md §15b)"
fi
}
# ---------------------------------------------------------------------------
# 5. Source/binary + image build + containerd import
# ---------------------------------------------------------------------------
@@ -2584,9 +2605,30 @@ push_images_to_registry() {
[ -n "$img" ] || continue
push_image_to_registry "$img"
done
for img in "$FELIS_LIMBO_IMAGE" "$FELIS_LOBBY_IMAGE" "$FELIS_PAPER_IMAGE"; do
[ -n "$img" ] || continue
push_version_tag "$img"
done
systemctl stop docker docker.socket 2>/dev/null || true
}
# push_version_tag mirrors a game image a second time under a tag no later run
# rewrites: <Minecraft version>-<first 12 hex of the image id>, e.g.
# felis/paper:26.2-3f9c0a1b2c4d. The :demo tag moves with every run, and servers are
# pinned to the digest it named when they were created, so this is the readable name
# for each build: an admin can whitelist it to create servers on that exact
# Minecraft version long after :demo has moved on.
push_version_tag() {
local ref="$1" id versioned
[ -n "${MC_VERSION:-}" ] || return 0
id="$(docker image inspect -f '{{.Id}}' "$ref" 2>/dev/null)" || return 0
id="${id#sha256:}"
versioned="${ref%:*}:${MC_VERSION}-${id:0:12}"
docker tag "$ref" "$versioned" || die "could not tag ${ref} as ${versioned} — is docker healthy?"
push_image_to_registry "$versioned"
docker rmi "$versioned" >/dev/null 2>&1 || true
}
# The login/lobby images use mutable :demo tags. Importing/pushing a replacement
# updates containerd, but an existing StatefulSet template is byte-for-byte
# unchanged and Kubernetes will not roll it. Recreate only the two always-on
@@ -3014,6 +3056,9 @@ main() {
build_image
# After build_image imported the felis image: the registry pod's gate runs it.
pin_registry_images
# Before build_game_stack: the builds user servers run must be read off the
# registry's tags before new ones replace them.
pin_user_server_images
build_game_stack
install_postgres
configure_postgres
+67
View File
@@ -824,6 +824,7 @@ out="$(
FELIS_IMAGE=a FELIS_LIMBO_IMAGE=b FELIS_LOBBY_IMAGE=c FELIS_PAPER_IMAGE=d bash -c '
systemctl() { printf "SYSTEMCTL %s\n" "$*"; }
push_image_to_registry() { printf "PUSH %s\n" "$1"; }
push_version_tag() { printf "VERSION %s\n" "$1"; }
registry_docker_login() { printf "LOGIN\n"; }
'"$wiblock"'
push_images_to_registry'
@@ -836,6 +837,34 @@ stops="$(printf '%s\n' "$out" | grep -c 'SYSTEMCTL stop docker')"
[ "$starts" = 1 ] && [ "$stops" = 1 ] && [ "$(printf '%s\n' "$out" | grep -c '^PUSH')" = 4 ] \
&& echo "PASS the batch wraps all four pushes in ONE docker start/stop" \
|| { echo "FAIL: expected 1 start / 1 stop / 4 pushes, got:"; printf '%s\n' "$out"; fails=$((fails + 1)); }
[ "$(printf '%s\n' "$out" | grep '^VERSION' | tr '\n' ' ')" = "VERSION b VERSION c VERSION d " ] \
&& echo "PASS the three game images, and only they, also get a version tag" \
|| { echo "FAIL: expected version tags for b c d only, got:"; printf '%s\n' "$out"; fails=$((fails + 1)); }
# Each game build is also mirrored under <MC version>-<image id>, a tag no later run
# rewrites, so an admin can still name that exact build after :demo moves on.
vtblock="$(awk '/^push_version_tag\(\) \{/,/^}/' "$BS")"
[ -n "$vtblock" ] || { echo "FAIL: no push_version_tag found in $BS"; exit 1; }
run_version_tag() { # MC_VERSION
MC_VERSION="$1" bash -c '
die() { printf "DIE: %s\n" "$*"; exit 1; }
docker() {
case "$1" in
image) printf "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n" ;;
*) printf "DOCKER %s\n" "$*" ;;
esac
}
push_image_to_registry() { printf "PUSH %s\n" "$1"; }
'"$vtblock"'
push_version_tag registry.felis.svc:5000/felis/paper:demo'
}
out="$(run_version_tag 26.2)"
expect "a game build is pushed under its version tag" "PUSH registry.felis.svc:5000/felis/paper:26.2-0123456789ab" "$out"
expect "the version tag is created from the built image" "DOCKER tag registry.felis.svc:5000/felis/paper:demo registry.felis.svc:5000/felis/paper:26.2-0123456789ab" "$out"
case "$(run_version_tag '')" in
*PUSH*) echo "FAIL: no Minecraft version, no version tag"; fails=$((fails + 1)) ;;
*) echo "PASS without a resolved Minecraft version no version tag is pushed" ;;
esac
# The registry refuses anonymous writes, and the platform token must never reach
# docker's argv (ps) or root's ~/.docker: stdin into a throwaway --config dir.
@@ -888,6 +917,44 @@ case "$out" in
*"limbo:demo io.cri"*) echo "FAIL: only the registry pod's images may be pinned or unpinned"; fails=$((fails + 1)) ;;
esac
# User servers still on a bare registry tag are pinned to the build it names BEFORE
# this run builds and pushes new ones over it; a fresh install has nothing to pin.
puiblock="$(awk '/^pin_user_server_images\(\) \{/,/^}/' "$BS")"
[ -n "$puiblock" ] || { echo "FAIL: no pin_user_server_images found in $BS"; exit 1; }
run_pin_user() { # crd-present(0/1) pin-exit
CALLS="$calls" CRD="$1" PIN_EXIT="$2" HOST_BIN=felis CONTROL_NS=felis MINECRAFT_NS=minecraft \
REGISTRY_URL=registry.felis.svc:5000 REGISTRY_PUSH_HOST=127.0.0.1:5000 bash -c '
ok() { printf "OK: %s\n" "$*"; }
warn() { printf "WARN: %s\n" "$*"; }
kube() {
case "$*" in
"get crd"*) [ "$CRD" = 1 ] ;;
*) printf "KUBE %s\n" "$*" >>"$CALLS" ;;
esac
}
felis() { printf "FELIS %s\n" "$*"; return "$PIN_EXIT"; }
'"$puiblock"'
pin_user_server_images'
}
calls="$(mktemp)"
case "$(run_pin_user 0 0)" in
*FELIS*) echo "FAIL: without the CRD there is nothing to pin"; fails=$((fails + 1)) ;;
*) echo "PASS a fresh install skips pinning" ;;
esac
out="$(run_pin_user 1 0)$(printf '\n'; cat "$calls")"
expect "pinning reaches the registry through its loopback hostPort" "FELIS pin-images --namespace minecraft --registry registry.felis.svc:5000 --endpoint 127.0.0.1:5000" "$out"
expect "pinning waits for the registry first" "KUBE -n felis rollout status deployment/registry" "$out"
out="$(run_pin_user 1 1)"
expect "a failed pin warns with the consequence" "WARN: could not pin every user server" "$out"
rm -f "$calls"
mainblock="$(awk '/^main\(\) \{/,/^}/' "$BS")"
line_of() { printf '%s\n' "$mainblock" | grep -n "^ $1\$" | head -n 1 | cut -d: -f1; }
p="$(line_of pin_user_server_images)"; b="$(line_of build_game_stack)"; u="$(line_of push_images_to_registry)"
[ -n "$p" ] && [ -n "$b" ] && [ -n "$u" ] && [ "$p" -lt "$b" ] && [ "$b" -lt "$u" ] \
&& echo "PASS user servers are pinned before the game images are rebuilt and pushed" \
|| { echo "FAIL: main must run pin_user_server_images before build_game_stack and push_images_to_registry (lines: $p $b $u)"; fails=$((fails + 1)); }
# --- the registry's own image must not be re-pulled on every run --------------------------
iblock="$(awk '/^import_registry_image\(\) \{/,/^}/' "$BS")"
[ -n "$iblock" ] || { echo "FAIL: no import_registry_image found in $BS"; exit 1; }