feat(bootstrap): 从发布产物安装二进制、镜像与插件,Docker 按需安装
This commit is contained in:
2 files changed
+1217
-100
No files matched your search
+665
-84
@@ -71,13 +71,22 @@
|
||||
# restarts cloudflared-felis onto the new binary (default: 0)
|
||||
# FELIS_REPO_URL git URL to build from (raw script mode only)
|
||||
# FELIS_VERSION_BOOTSTRAP release|dev — which version to install (default: release).
|
||||
# release DOWNLOADS the prebuilt felis binary published for the newest
|
||||
# tag (panel included — it is go:embed'ed into that same binary) and
|
||||
# builds only a thin image around it; dev clones and compiles. If the
|
||||
# asset is missing or this architecture has none, release warns and falls
|
||||
# back to compiling the SAME tag. The downloaded binary must match
|
||||
# the release's SHA256SUMS before it is run; a release without one
|
||||
# is compiled from source too. The game stack is always built here.
|
||||
# release DOWNLOADS what the newest tag publishes: the felis binary
|
||||
# (panel included — it is go:embed'ed into that same binary), every
|
||||
# image and the Velocity plugin, each checked against the release's
|
||||
# SHA256SUMS before it is used, so the host needs neither Docker nor
|
||||
# Docker Hub; dev clones and compiles. If an asset is missing or this
|
||||
# architecture has none, release warns and builds that piece of the
|
||||
# SAME tag here instead (with Docker); a release without SHA256SUMS
|
||||
# is compiled from source whole.
|
||||
# FELIS_ARTIFACT_DIR a directory holding a release's assets as release.yml publishes them
|
||||
# (felis-linux-<arch>, felis-image-*-linux-<arch>.tar, their listing
|
||||
# felis-images-linux-<arch>.txt, felis-velocity.jar, SHA256SUMS; see
|
||||
# deploy/build-release-artifacts.sh): the binary, images and plugin are
|
||||
# installed from there and nothing is fetched from api.github.com or
|
||||
# Docker Hub, nor built (FELIS_GAME_STACK=latest aside: no release
|
||||
# ships that stack, so its game images are built here). A file missing
|
||||
# from it or not matching its SHA256SUMS stops the install.
|
||||
# FELIS_GITHUB_TOKEN GitHub token; REQUIRED while the repo is private
|
||||
# FELIS_REF branch/tag/sha — pins the build, overrides the channel, and forces a
|
||||
# source build (naming a ref asks for that tree, not a published asset)
|
||||
@@ -144,13 +153,43 @@ FELIS_REF="${FELIS_REF:-}"
|
||||
# built, so it takes the source path even on the release channel.
|
||||
FELIS_REF_PINNED=""
|
||||
if [ -n "$FELIS_REF" ]; then FELIS_REF_PINNED=1; fi
|
||||
# Set once a prebuilt felis binary is installed at HOST_BIN, by either the TUI hand-off or a
|
||||
# release download. It is what the image build, the CRD apply and the game stack key off:
|
||||
# all three only need "is there a binary and no checkout", never "which route got us here".
|
||||
# The directory of release assets to install from (header); empty installs as the channel says.
|
||||
FELIS_ARTIFACT_DIR="${FELIS_ARTIFACT_DIR:-}"
|
||||
# Set once a prebuilt felis binary is installed at HOST_BIN, by the TUI hand-off, a release
|
||||
# download or FELIS_ARTIFACT_DIR. It is what the image build, the CRD apply and the game stack
|
||||
# key off: all three only need "is there a binary and no checkout", never "which route got us
|
||||
# here".
|
||||
HAVE_PREBUILT_BINARY=""
|
||||
# The image the control plane ran before this run moved it (deploy_bundle), for the rollback
|
||||
# hint in summary.
|
||||
PREVIOUS_FELIS_IMAGE=""
|
||||
# Where the images and the Velocity plugin come from, decided by select_release_artifacts once
|
||||
# the binary is on the host: "dir" (FELIS_ARTIFACT_DIR), "release" (the assets of release
|
||||
# ARTIFACT_TAG, downloaded into ARTIFACT_CACHE), or empty, when everything is built here.
|
||||
ARTIFACT_MODE=""
|
||||
ARTIFACT_TAG=""
|
||||
# Root-only, and outside /tmp: an image bundle waits here from its import to its push, and a
|
||||
# run that failed in between finds it again. push_images_to_registry empties it.
|
||||
ARTIFACT_CACHE="/var/lib/felis/artifacts"
|
||||
# The SHA256SUMS every artifact is checked against (load_artifact_sums), and the verified local
|
||||
# copy artifact_fetch last pointed at.
|
||||
ARTIFACT_SUMS=""
|
||||
ARTIFACT_FILE=""
|
||||
# The validated lines of the image listing ("bundle role name manifest-digest config-digest"),
|
||||
# and whether it was read yet: "", "ok", or "bad" (the listing could not be used).
|
||||
RELEASE_LISTING=""
|
||||
RELEASE_LISTING_STATE=""
|
||||
# One "role bundle name target manifest-digest config-digest" line per image this run took from
|
||||
# a bundle (import_release_images), and those roles; every other image is built here.
|
||||
RELEASE_IMAGES=""
|
||||
PREBUILT_ROLES=" "
|
||||
# Docker is installed and started only for what has to be built on this host (ensure_docker).
|
||||
DOCKER_INSTALLED=""
|
||||
# The release JSON every asset lookup reads, fetched once per tag (load_release_json): an install
|
||||
# downloads up to a dozen assets, and GitHub allows an address without a token 60 API calls an
|
||||
# hour.
|
||||
RELEASE_JSON_TAG=""
|
||||
RELEASE_JSON=""
|
||||
# Optional GitHub credential, needed while this repository is private: GitHub answers
|
||||
# 404 (not 403) for a repo the caller cannot see, so without it both the release lookup
|
||||
# and the clone fail as "not found". Exported because git's credential helper below runs
|
||||
@@ -436,6 +475,8 @@ K3S_CONFIG_DROPIN="/etc/rancher/k3s/config.yaml.d/50-felis.yaml"
|
||||
K3S_UNIT_FILE="/etc/systemd/system/k3s.service"
|
||||
K3S_KUBECONFIG="/etc/rancher/k3s/k3s.yaml"
|
||||
K3S_KUBELET_CERT="/var/lib/rancher/k3s/agent/client-kubelet.crt"
|
||||
# Where k3s imports image tarballs from as it starts (stage_k3s_airgap_images).
|
||||
K3S_IMAGES_DIR="/var/lib/rancher/k3s/agent/images"
|
||||
# ensure_persistent_journal's drop-in, and the directory journald creates once it
|
||||
# stores the journal persistently.
|
||||
JOURNALD_DROPIN="/etc/systemd/journald.conf.d/50-felis.conf"
|
||||
@@ -830,6 +871,17 @@ validate_settings() {
|
||||
pinned|latest) ;;
|
||||
*) die "FELIS_GAME_STACK must be pinned or latest (got '${FELIS_GAME_STACK}')" ;;
|
||||
esac
|
||||
if [ -n "$FELIS_ARTIFACT_DIR" ]; then
|
||||
case "$FELIS_ARTIFACT_DIR" in
|
||||
/*) ;;
|
||||
*) die "FELIS_ARTIFACT_DIR must be an absolute path (got '${FELIS_ARTIFACT_DIR}')" ;;
|
||||
esac
|
||||
[ -f "${FELIS_ARTIFACT_DIR}/SHA256SUMS" ] \
|
||||
|| die "FELIS_ARTIFACT_DIR: ${FELIS_ARTIFACT_DIR}/SHA256SUMS does not exist; point it at the directory deploy/build-release-artifacts.sh wrote, or at a release's downloaded assets"
|
||||
# Each names what to install; the directory's binary would silently win.
|
||||
[ -z "$FELIS_REF_PINNED" ] || die "FELIS_ARTIFACT_DIR and FELIS_REF both name what to install; set one"
|
||||
[ -z "${FELIS_SKIP_FETCH:-}" ] || die "FELIS_ARTIFACT_DIR and FELIS_SKIP_FETCH both name what to install; set one"
|
||||
fi
|
||||
[ "$(heap_megabytes "$FELIS_VELOCITY_XMX")" -ge 256 ] \
|
||||
|| die "FELIS_VELOCITY_XMX must be a heap size of at least 256M, written <n>M or <n>G (got '${FELIS_VELOCITY_XMX}')"
|
||||
case "$FELIS_UPGRADE_DEPS" in
|
||||
@@ -1080,16 +1132,38 @@ existing_ancestor() {
|
||||
# path_populated reports whether directory $1 exists with something in it.
|
||||
path_populated() { [ -n "$(ls -A "$1" 2>/dev/null)" ]; }
|
||||
|
||||
# release_assets_expected reports whether this run expects to download a release's images and
|
||||
# Velocity plugin (select_release_artifacts): on the release channel, and from the setup
|
||||
# console, whose binary is a release's.
|
||||
release_assets_expected() {
|
||||
[ -z "$FELIS_ARTIFACT_DIR" ] || return 1
|
||||
bootstrap_from_tui || use_release_binary
|
||||
}
|
||||
|
||||
# host_builds_expected reports whether this run expects to build images here, and so to install
|
||||
# Docker: a source build does, an install from a release's assets does not, and the game images
|
||||
# under FELIS_GAME_STACK=latest are always built here, since no release ships that stack. What
|
||||
# preflight cannot see coming is a release that turns out to carry no usable images (one cut
|
||||
# before they were published, or still uploading); that one is built here after all.
|
||||
host_builds_expected() {
|
||||
[ "$FELIS_GAME_STACK" != latest ] || return 0
|
||||
[ -z "$FELIS_ARTIFACT_DIR" ] || return 1
|
||||
! release_assets_expected
|
||||
}
|
||||
|
||||
# preflight_disk checks each filesystem the install writes to against what it will
|
||||
# write there, in MiB: k3s's images and volumes, the database and its bundles under
|
||||
# /var/lib/felis, the sources, toolchains and proxy under /opt/felis, and Docker's image
|
||||
# builds (operations.md §2 has the measured sizes). A directory that already holds
|
||||
# something (a rerun, a reused k3s, Docker's cache from an earlier install) needs only
|
||||
# the room for what changes.
|
||||
# /var/lib/felis (and a release's image bundles on their way in), the sources, toolchains
|
||||
# and proxy under /opt/felis, and Docker's image builds (operations.md §2 has the measured
|
||||
# sizes). A directory that already holds something (a rerun, a reused k3s, Docker's cache
|
||||
# from an earlier install) needs only the room for what changes.
|
||||
preflight_disk() {
|
||||
local spec path need_empty need_populated need line rows="" mount size used avail
|
||||
for spec in "/var/lib/rancher 10240 3072" "/var/lib/felis 2048 1024" "/opt/felis 3072 1024" \
|
||||
"/var/lib/containerd 8192 2048"; do
|
||||
local felis_spec="/var/lib/felis 2048 1024" docker_spec=""
|
||||
release_assets_expected && felis_spec="/var/lib/felis 4096 3072"
|
||||
host_builds_expected && docker_spec="/var/lib/containerd 8192 2048"
|
||||
for spec in "/var/lib/rancher 10240 3072" "$felis_spec" "/opt/felis 3072 1024" \
|
||||
${docker_spec:+"$docker_spec"}; do
|
||||
read -r path need_empty need_populated <<<"$spec"
|
||||
need="$need_empty"
|
||||
path_populated "$path" && need="$need_populated"
|
||||
@@ -1216,16 +1290,26 @@ preflight_networks() {
|
||||
done <<<"$routes"
|
||||
}
|
||||
|
||||
# preflight_hosts prints the hosts this run downloads from, one per line. Package
|
||||
# mirrors are left out: the package manager names its own.
|
||||
# preflight_hosts prints the hosts this run downloads from, one "host required|optional" line
|
||||
# each. An optional host is one the install only falls back to: Docker Hub when the images are
|
||||
# expected from a release, and, from the setup console, the release lookup (without it every
|
||||
# image is built here). Package mirrors are left out: the package manager names its own.
|
||||
preflight_hosts() {
|
||||
printf '%s\n' github.com
|
||||
if ! bootstrap_from_tui && [ -z "${FELIS_SKIP_FETCH:-}" ] && [ -z "$FELIS_REF_PINNED" ]; then
|
||||
printf '%s\n' api.github.com
|
||||
printf '%s\n' "github.com required"
|
||||
if [ -z "$FELIS_ARTIFACT_DIR" ] && [ -z "${FELIS_SKIP_FETCH:-}" ] && [ -z "$FELIS_REF_PINNED" ]; then
|
||||
if bootstrap_from_tui; then
|
||||
printf '%s\n' "api.github.com optional"
|
||||
else
|
||||
printf '%s\n' "api.github.com required"
|
||||
fi
|
||||
fi
|
||||
[ -x "$K3S_BIN" ] || printf '%s\n' "raw.githubusercontent.com required"
|
||||
[ -n "$FELIS_VELOCITY_FORK_JAR" ] || printf '%s\n' "fill-data.papermc.io required"
|
||||
if host_builds_expected; then
|
||||
printf '%s\n' "registry-1.docker.io required"
|
||||
elif [ -z "$FELIS_ARTIFACT_DIR" ]; then
|
||||
printf '%s\n' "registry-1.docker.io optional"
|
||||
fi
|
||||
[ -x "$K3S_BIN" ] || printf '%s\n' raw.githubusercontent.com
|
||||
[ -n "$FELIS_VELOCITY_FORK_JAR" ] || printf '%s\n' fill-data.papermc.io
|
||||
printf '%s\n' registry-1.docker.io
|
||||
}
|
||||
|
||||
# host_reachable reports whether an HTTPS connection to $1 can be made: any answer
|
||||
@@ -1243,11 +1327,19 @@ host_reachable() {
|
||||
}
|
||||
|
||||
preflight_outbound() {
|
||||
local host unreachable=()
|
||||
while IFS= read -r host; do
|
||||
local host need unreachable=() fallback=()
|
||||
while read -r host need; do
|
||||
[ -n "$host" ] || continue
|
||||
host_reachable "$host" || unreachable+=("$host")
|
||||
host_reachable "$host" && continue
|
||||
if [ "$need" = optional ]; then
|
||||
fallback+=("$host")
|
||||
else
|
||||
unreachable+=("$host")
|
||||
fi
|
||||
done < <(preflight_hosts)
|
||||
if [ "${#fallback[@]}" -gt 0 ]; then
|
||||
warn "preflight: cannot reach ${fallback[*]} over HTTPS; the install goes on, but cannot build or pull here an image the release turns out not to supply"
|
||||
fi
|
||||
[ "${#unreachable[@]}" -eq 0 ] && return 0
|
||||
preflight_fail "cannot reach ${unreachable[*]} over HTTPS; the install downloads from there (check DNS, the firewall, or set https_proxy)"
|
||||
}
|
||||
@@ -1587,6 +1679,25 @@ install_docker() {
|
||||
ok "docker running"
|
||||
}
|
||||
|
||||
# ensure_docker installs and starts Docker the first time this run has something to build, and
|
||||
# starts it again after an earlier step stopped it. Only what a release did not ship prebuilt
|
||||
# is built here, so an install from a release's assets never installs Docker at all.
|
||||
ensure_docker() {
|
||||
if [ -z "$DOCKER_INSTALLED" ]; then
|
||||
install_docker
|
||||
DOCKER_INSTALLED=1
|
||||
else
|
||||
systemctl start docker
|
||||
fi
|
||||
}
|
||||
|
||||
# stop_docker hands back the ~150 MiB the docker daemon holds once a step is done with it; the
|
||||
# next step that builds starts it again. A Docker this run never started is left alone.
|
||||
stop_docker() {
|
||||
[ -n "$DOCKER_INSTALLED" ] || return 0
|
||||
systemctl stop docker docker.socket 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. k3s — single node, trimmed for RAM. NetworkPolicy stays ENABLED on purpose:
|
||||
# Felis's minecraft fence (default-deny + allow-rcon/allow-game) is a core
|
||||
@@ -1622,11 +1733,13 @@ install_k3s() {
|
||||
ok "k3s ${current:-(version unreadable)} already installed at ${K3S_BIN}; this release pins ${FELIS_K3S_VERSION} (FELIS_UPGRADE_DEPS=1 moves it)"
|
||||
elif k3s_upgrade_allowed "$current" "$FELIS_K3S_VERSION"; then
|
||||
log "upgrading k3s ${current} to ${FELIS_K3S_VERSION}; running pods keep running while it restarts"
|
||||
stage_k3s_airgap_images
|
||||
run_k3s_installer
|
||||
installer_ran=1
|
||||
fi
|
||||
else
|
||||
log "installing k3s ${FELIS_K3S_VERSION} into ${K3S_BIN_DIR} (no traefik/servicelb/metrics-server)"
|
||||
stage_k3s_airgap_images
|
||||
run_k3s_installer
|
||||
installer_ran=1
|
||||
fi
|
||||
@@ -1742,6 +1855,47 @@ run_k3s_installer() {
|
||||
sh -
|
||||
}
|
||||
|
||||
# stage_k3s_airgap_images puts the image tarball of the k3s release about to be installed where
|
||||
# k3s imports images from as it starts, so k3s's own images (pause, CoreDNS, the local-path
|
||||
# provisioner) come from that GitHub release, checked against its sha256sum file, instead of
|
||||
# from Docker Hub, whose anonymous pull limit (10 an hour per address) a shared VPS address may
|
||||
# have spent already. Best-effort: without the file k3s pulls them as it always has. k3s only
|
||||
# reads names ending in a tarball extension, so the download's dotted temp name is never read.
|
||||
stage_k3s_airgap_images() {
|
||||
local arch base file sums want have tmp
|
||||
arch="$(felis_asset_arch)" || return 0
|
||||
base="https://github.com/k3s-io/k3s/releases/download/${FELIS_K3S_VERSION}"
|
||||
file="k3s-airgap-images-${arch}.tar.zst"
|
||||
sums="$(curl -fsSL --retry 5 --retry-delay 2 "${base}/sha256sum-${arch}.txt")" || sums=""
|
||||
want="$(awk -v n="$file" '$2 == n { print $1; exit }' <<<"$sums")"
|
||||
if ! [[ "$want" =~ ^[0-9a-f]{64}$ ]]; then
|
||||
warn "k3s ${FELIS_K3S_VERSION} lists no sha256 for ${file}; k3s pulls its own images from Docker Hub instead"
|
||||
return 0
|
||||
fi
|
||||
if [ -f "${K3S_IMAGES_DIR}/${file}" ] && [ "$(sha256sum <"${K3S_IMAGES_DIR}/${file}" | cut -d' ' -f1)" = "$want" ]; then
|
||||
ok "k3s ${FELIS_K3S_VERSION}'s images already staged"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$K3S_IMAGES_DIR"
|
||||
tmp="$(mktemp "${K3S_IMAGES_DIR}/.${file}.XXXXXX")"
|
||||
remember_temp "$tmp"
|
||||
log "downloading k3s ${FELIS_K3S_VERSION}'s own images (${file})"
|
||||
if ! curl -fsSL --retry 5 --retry-delay 2 -o "$tmp" "${base}/${file}"; then
|
||||
rm -f "$tmp"
|
||||
warn "could not download ${file}; k3s pulls its own images from Docker Hub instead"
|
||||
return 0
|
||||
fi
|
||||
have="$(sha256sum <"$tmp" | cut -d' ' -f1)"
|
||||
if [ "$have" != "$want" ]; then
|
||||
rm -f "$tmp"
|
||||
warn "${file} hashes to ${have}, but k3s ${FELIS_K3S_VERSION}'s sha256sum-${arch}.txt says ${want}; k3s pulls its own images from Docker Hub instead"
|
||||
return 0
|
||||
fi
|
||||
chmod 0644 "$tmp"
|
||||
mv -f "$tmp" "${K3S_IMAGES_DIR}/${file}"
|
||||
ok "staged k3s ${FELIS_K3S_VERSION}'s images for its first start"
|
||||
}
|
||||
|
||||
# k3s_upgrade_allowed decides whether an installed k3s ($1) may move to $2. Kubernetes
|
||||
# supports upgrading one minor version at a time, so a larger jump stops the install
|
||||
# before anything changed; a newer installed k3s is left as it is.
|
||||
@@ -1967,6 +2121,14 @@ felis_asset_arch() {
|
||||
esac
|
||||
}
|
||||
|
||||
# load_release_json keeps release tag $1's JSON in RELEASE_JSON, fetching it only for a tag it
|
||||
# does not hold yet.
|
||||
load_release_json() {
|
||||
[ "$RELEASE_JSON_TAG" != "$1" ] || return 0
|
||||
RELEASE_JSON="$(github_api "repos/$(repo_slug)/releases/tags/$1")" || return 1
|
||||
RELEASE_JSON_TAG="$1"
|
||||
}
|
||||
|
||||
# github_asset_id prints the numeric id of the asset named $2 on release tag $1.
|
||||
#
|
||||
# Two details here are load-bearing and both are wrong in the obvious version. The newline
|
||||
@@ -1981,10 +2143,10 @@ felis_asset_arch() {
|
||||
# precedes it. Anything published after uploader (size, digest, browser_download_url) is NOT
|
||||
# reachable this way — fetch releases/assets/<id> with the JSON Accept if that is ever needed.
|
||||
github_asset_id() {
|
||||
local tag="$1" name="$2" json id
|
||||
local tag="$1" name="$2" id
|
||||
# Fetch first, filter second — the SIGPIPE reason documented on resolve_latest_game_jars.
|
||||
json="$(github_api "repos/$(repo_slug)/releases/tags/${tag}")" || return 1
|
||||
id="$(printf '%s' "$json" | tr -d '\n' | tr '{' '\n' \
|
||||
load_release_json "$tag" || return 1
|
||||
id="$(printf '%s' "$RELEASE_JSON" | tr -d '\n' | tr '{' '\n' \
|
||||
| grep "\"name\":[[:space:]]*\"${name}\"" \
|
||||
| grep -o 'releases/assets/[0-9]\{1,\}' | head -1)" || true
|
||||
id="${id##*/}"
|
||||
@@ -2013,6 +2175,8 @@ github_asset_id() {
|
||||
download_release_asset() {
|
||||
local tag="$1" name="$2" dest="$3" id ua url rc=0
|
||||
ua="felis-bootstrap (+${FELIS_REPO_URL})"
|
||||
# Loaded here, in this shell, so the lookup's subshell below finds it cached.
|
||||
load_release_json "$tag" || return 1
|
||||
id="$(github_asset_id "$tag" "$name")" || return 1
|
||||
url="https://api.github.com/repos/$(repo_slug)/releases/assets/${id}"
|
||||
log "downloading ${name} from release ${tag}"
|
||||
@@ -2148,6 +2312,159 @@ download_release_binary() {
|
||||
ok "installed ${asset} ${FELIS_REF} at ${HOST_BIN}"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5a. A release's prebuilt assets: every image and the Velocity plugin, next to the binary
|
||||
# (deploy/build-release-artifacts.sh writes them, release.yml publishes them). Each file
|
||||
# is used only once its sha256 matches the release's SHA256SUMS.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# load_artifact_sums reads the SHA256SUMS the artifacts are checked against: the directory's,
|
||||
# or release ARTIFACT_TAG's. It fails when there is none.
|
||||
load_artifact_sums() {
|
||||
local tmp
|
||||
ARTIFACT_SUMS=""
|
||||
if [ "$ARTIFACT_MODE" = dir ]; then
|
||||
ARTIFACT_SUMS="$(cat "${FELIS_ARTIFACT_DIR}/SHA256SUMS" 2>/dev/null)" || ARTIFACT_SUMS=""
|
||||
else
|
||||
tmp="$(mktemp)"
|
||||
remember_temp "$tmp"
|
||||
if download_release_asset "$ARTIFACT_TAG" SHA256SUMS "$tmp"; then
|
||||
ARTIFACT_SUMS="$(cat "$tmp")"
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
[ -n "$ARTIFACT_SUMS" ]
|
||||
}
|
||||
|
||||
# artifact_sum <name> prints the sha256 SHA256SUMS lists for <name>, or nothing. sha256sum's
|
||||
# text-mode line is "<hash> <name>", binary mode "<hash> *<name>". No regex interval here:
|
||||
# Debian's mawk does not take one.
|
||||
artifact_sum() {
|
||||
awk -v n="$1" '($2 == n || $2 == "*" n) && length($1) == 64 && $1 !~ /[^0-9a-f]/ { print $1; exit }' <<<"$ARTIFACT_SUMS"
|
||||
}
|
||||
|
||||
# artifact_fetch <name> points ARTIFACT_FILE at a local copy of artifact <name> whose sha256 is
|
||||
# the one SHA256SUMS lists: the directory's own file, or a download kept in ARTIFACT_CACHE (a
|
||||
# copy already there that still matches is used as it is, so a rerun after a failure fetches
|
||||
# nothing twice). It warns and fails, leaving nothing half-written behind, when SHA256SUMS
|
||||
# does not list the name, the file is missing, or its bytes differ.
|
||||
artifact_fetch() {
|
||||
local name="$1" want have file partial
|
||||
ARTIFACT_FILE=""
|
||||
want="$(artifact_sum "$name")"
|
||||
if [ -z "$want" ]; then
|
||||
warn "SHA256SUMS lists no ${name}"
|
||||
return 1
|
||||
fi
|
||||
if [ "$ARTIFACT_MODE" = dir ]; then
|
||||
file="${FELIS_ARTIFACT_DIR}/${name}"
|
||||
if [ ! -f "$file" ]; then
|
||||
warn "${file} is missing"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
file="${ARTIFACT_CACHE}/${name}"
|
||||
fi
|
||||
if [ -f "$file" ]; then
|
||||
have="$(sha256sum <"$file" | cut -d' ' -f1)"
|
||||
if [ "$have" = "$want" ]; then
|
||||
ARTIFACT_FILE="$file"
|
||||
return 0
|
||||
fi
|
||||
if [ "$ARTIFACT_MODE" = dir ]; then
|
||||
warn "${file} hashes to ${have}, but SHA256SUMS says ${want}"
|
||||
return 1
|
||||
fi
|
||||
rm -f "$file"
|
||||
fi
|
||||
install -d -m 0700 "$ARTIFACT_CACHE"
|
||||
partial="${file}.partial"
|
||||
if ! download_release_asset "$ARTIFACT_TAG" "$name" "$partial"; then
|
||||
rm -f "$partial"
|
||||
warn "could not download ${name} from release ${ARTIFACT_TAG}"
|
||||
return 1
|
||||
fi
|
||||
have="$(sha256sum <"$partial" | cut -d' ' -f1)"
|
||||
if [ "$have" != "$want" ]; then
|
||||
rm -f "$partial"
|
||||
warn "downloaded ${name} hashes to ${have}, but release ${ARTIFACT_TAG}'s SHA256SUMS says ${want}"
|
||||
return 1
|
||||
fi
|
||||
mv -f "$partial" "$file"
|
||||
ARTIFACT_FILE="$file"
|
||||
}
|
||||
|
||||
# artifact_unusable <problem> <fallback> handles an asset this run cannot use. With
|
||||
# FELIS_ARTIFACT_DIR it stops the install: the operator named the directory so that nothing
|
||||
# would be built or pulled here. For a downloaded release it warns, and <fallback> (that piece
|
||||
# built on this host, or pulled) takes its place.
|
||||
artifact_unusable() {
|
||||
[ "$ARTIFACT_MODE" != dir ] || die "FELIS_ARTIFACT_DIR: $1"
|
||||
warn "$1; $2"
|
||||
}
|
||||
|
||||
# install_artifact_binary installs FELIS_ARTIFACT_DIR's felis binary, and never falls back to a
|
||||
# build: a missing or mismatched file is for the operator to fix. The copy is staged next to
|
||||
# HOST_BIN for download_release_binary's reason (the directory, like /tmp, may be mounted
|
||||
# noexec), and hashed there, after the copy, so the bytes checked are the bytes run. Its
|
||||
# version names the control-plane image and the release the rest of the directory must be.
|
||||
install_artifact_binary() {
|
||||
local arch name tmp got
|
||||
ARTIFACT_MODE=dir
|
||||
arch="$(felis_asset_arch)" || die "FELIS_ARTIFACT_DIR: Felis publishes no binary for $(uname -m)"
|
||||
name="felis-linux-${arch}"
|
||||
load_artifact_sums || die "FELIS_ARTIFACT_DIR: ${FELIS_ARTIFACT_DIR} holds no SHA256SUMS"
|
||||
[ -n "$(artifact_sum "$name")" ] || die "FELIS_ARTIFACT_DIR: SHA256SUMS lists no ${name}"
|
||||
mkdir -p "$(dirname "$HOST_BIN")"
|
||||
tmp="$(mktemp "$(dirname "$HOST_BIN")/.felis-download.XXXXXX")"
|
||||
remember_temp "$tmp"
|
||||
cp "${FELIS_ARTIFACT_DIR}/${name}" "$tmp" || die "FELIS_ARTIFACT_DIR: cannot read ${FELIS_ARTIFACT_DIR}/${name}"
|
||||
[ "$(sha256sum <"$tmp" | cut -d' ' -f1)" = "$(artifact_sum "$name")" ] \
|
||||
|| die "FELIS_ARTIFACT_DIR: ${name} does not match SHA256SUMS"
|
||||
chmod 0755 "$tmp"
|
||||
got="$("$tmp" version 2>/dev/null | head -n 1 || true)"
|
||||
case "$got" in
|
||||
"felis "?*) ;;
|
||||
*) die "FELIS_ARTIFACT_DIR: ${name} reports '${got:-nothing}' as its version" ;;
|
||||
esac
|
||||
FELIS_VERSION="${got#felis }"
|
||||
if [ -x "$HOST_BIN" ] && cmp -s "$tmp" "$HOST_BIN"; then
|
||||
ok "host binary is already felis ${FELIS_VERSION} from ${FELIS_ARTIFACT_DIR}"
|
||||
else
|
||||
keep_previous_host_binary
|
||||
rm -f "$HOST_BIN"
|
||||
install -m 0755 "$tmp" "$HOST_BIN"
|
||||
command -v restorecon >/dev/null 2>&1 && restorecon "$HOST_BIN" >/dev/null 2>&1 || true
|
||||
ok "installed felis ${FELIS_VERSION} from ${FELIS_ARTIFACT_DIR}"
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
HAVE_PREBUILT_BINARY=1
|
||||
}
|
||||
|
||||
# select_release_artifacts decides, once the felis binary is on the host, where this run's
|
||||
# images and Velocity plugin come from: FELIS_ARTIFACT_DIR, else the assets of the release the
|
||||
# binary is (the one just downloaded, or the one the setup console runs). A source build
|
||||
# builds them too, and so does a release without SHA256SUMS (cut before release.yml wrote
|
||||
# one, or still uploading), since nothing of it could be checked.
|
||||
select_release_artifacts() {
|
||||
local v
|
||||
[ "$ARTIFACT_MODE" != dir ] || return 0
|
||||
[ -n "$HAVE_PREBUILT_BINARY" ] || return 0
|
||||
v="$("$HOST_BIN" version 2>/dev/null | head -n 1 || true)"
|
||||
v="${v#felis }"
|
||||
# A release's version is its tag; a dev or pinned build's names no release.
|
||||
[[ "$v" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || return 0
|
||||
ARTIFACT_MODE=release
|
||||
ARTIFACT_TAG="$v"
|
||||
if load_artifact_sums; then
|
||||
ok "release ${v}'s prebuilt images and Velocity plugin are installed as published"
|
||||
return 0
|
||||
fi
|
||||
ARTIFACT_MODE=""
|
||||
ARTIFACT_TAG=""
|
||||
warn "release ${v} publishes no SHA256SUMS, so none of its images can be checked; building them on this host instead (this installs Docker and needs about 8 GiB more under /var/lib/containerd)"
|
||||
}
|
||||
|
||||
# git_auth runs git with the token supplied by an inline credential helper. The helper
|
||||
# is a shell snippet that READS the exported variable when git asks; the token is never
|
||||
# in argv and never reaches .git/config, so it does not outlive the process.
|
||||
@@ -2364,7 +2681,11 @@ image_tag_for_version() {
|
||||
}
|
||||
|
||||
build_image() {
|
||||
systemctl start docker
|
||||
if role_prebuilt felis; then
|
||||
ok "${FELIS_IMAGE} is the release's own image; nothing to build"
|
||||
return 0
|
||||
fi
|
||||
ensure_docker
|
||||
# Keyed on the binary, not on the route that produced it: the TUI hand-off and a release
|
||||
# download both land on exactly the same state (a felis binary at HOST_BIN, no checkout),
|
||||
# and a release download that fell back to source has cleared this so the source build runs.
|
||||
@@ -2393,6 +2714,159 @@ remove_k3s_image() {
|
||||
esac
|
||||
}
|
||||
|
||||
# role_image <role> prints the name this install runs <role>'s image under: the names above for
|
||||
# the images Felis builds, the name CRI gives a digest-pinned pull for the two it does not.
|
||||
role_image() {
|
||||
case "$1" in
|
||||
felis) printf '%s\n' "$FELIS_IMAGE" ;;
|
||||
limbo) printf '%s\n' "$FELIS_LIMBO_IMAGE" ;;
|
||||
lobby) printf '%s\n' "$FELIS_LOBBY_IMAGE" ;;
|
||||
paper) printf '%s\n' "$FELIS_PAPER_IMAGE" ;;
|
||||
registry) pinned_image_ref "$REGISTRY_IMAGE" ;;
|
||||
postgres) pinned_image_ref "$POSTGRES_IMAGE" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# role_fallback <role> prints what takes the place of a release image this run cannot use.
|
||||
role_fallback() {
|
||||
case "$1" in
|
||||
registry|postgres) printf 'k3s pulls it from Docker Hub instead' ;;
|
||||
*) printf 'building it on this host instead' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# role_prebuilt <role> reports whether <role>'s image came from the release (import_release_images).
|
||||
role_prebuilt() {
|
||||
case "$PREBUILT_ROLES" in *" $1 "*) return 0 ;; esac
|
||||
return 1
|
||||
}
|
||||
|
||||
# image_repo <ref> prints <ref> with its digest and tag dropped.
|
||||
image_repo() {
|
||||
local r="${1%%@*}"
|
||||
case "${r##*/}" in *:*) r="${r%:*}" ;; esac
|
||||
printf '%s\n' "$r"
|
||||
}
|
||||
|
||||
# image_present <ctr-images-ls> <ref> <manifest-digest> reports whether the listing holds <ref>
|
||||
# (its header row starts with REF, which no image is named). A tag has to name
|
||||
# <manifest-digest> too, since a tag moves; a digest name is the content it names, whether a
|
||||
# bundle imported the platform manifest under it or CRI pulled the whole index.
|
||||
image_present() {
|
||||
case "$2" in
|
||||
*@*) awk -v r="$2" '$1 == r { f = 1 } END { exit !f }' <<<"$1" ;;
|
||||
*) awk -v r="$2" -v d="$3" '$1 == r && $3 == d { f = 1 } END { exit !f }' <<<"$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# load_release_listing <file> <arch> keeps the image listing's lines in RELEASE_LISTING once every
|
||||
# line is one bundle of this architecture, a role Felis knows once, two sha256 digests and a
|
||||
# name made of image-reference characters. A single line that is not is reason enough to trust
|
||||
# none: the listing is what decides which tar each image is read from.
|
||||
load_release_listing() {
|
||||
local file="$1" arch="$2" bundle role name digest config rest out="" seen=" "
|
||||
while read -r bundle role name digest config rest; do
|
||||
[ -n "$bundle" ] || continue
|
||||
[ -z "$rest" ] || return 1
|
||||
[[ "$bundle" =~ ^felis-image-[a-z]+-linux-${arch}\.tar$ ]] || return 1
|
||||
case "$role" in felis|limbo|lobby|paper|registry|postgres) ;; *) return 1 ;; esac
|
||||
case "$seen" in *" $role "*) return 1 ;; esac
|
||||
seen="${seen}${role} "
|
||||
[[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] && [[ "$config" =~ ^sha256:[0-9a-f]{64}$ ]] || return 1
|
||||
[[ "$name" =~ ^[A-Za-z0-9._:/@-]+$ ]] || return 1
|
||||
out="${out}${bundle} ${role} ${name} ${digest} ${config}"$'\n'
|
||||
done <"$file"
|
||||
[ -n "$out" ] || return 1
|
||||
RELEASE_LISTING="$out"
|
||||
}
|
||||
|
||||
# release_listing reads this architecture's image listing once per run, and fails, after one
|
||||
# warning (or, from FELIS_ARTIFACT_DIR, the end of the install), when it cannot be used.
|
||||
release_listing() {
|
||||
local arch name
|
||||
case "$RELEASE_LISTING_STATE" in
|
||||
ok) return 0 ;;
|
||||
bad) return 1 ;;
|
||||
esac
|
||||
RELEASE_LISTING_STATE=bad
|
||||
arch="$(felis_asset_arch)" || return 1
|
||||
name="felis-images-linux-${arch}.txt"
|
||||
if ! artifact_fetch "$name"; then
|
||||
artifact_unusable "the release's image listing ${name} cannot be used" "building its images on this host instead"
|
||||
return 1
|
||||
fi
|
||||
if ! load_release_listing "$ARTIFACT_FILE" "$arch"; then
|
||||
artifact_unusable "the release's image listing ${name} is malformed" "building its images on this host instead"
|
||||
return 1
|
||||
fi
|
||||
RELEASE_LISTING_STATE=ok
|
||||
}
|
||||
|
||||
# import_release_images <role>... puts each role's image into k3s containerd from the release's
|
||||
# bundles, and records it as prebuilt: build_image and build_game_stack skip it, and
|
||||
# push_images_to_registry pushes it from its bundle. Only the bundles holding an image
|
||||
# containerd lacks are fetched and imported, so a rerun, or an upgrade that changed only the
|
||||
# control plane, downloads just that. A bundle names each image by the name the installer
|
||||
# runs it under by default; a FELIS_*_IMAGE set to another name gets that name as a second tag
|
||||
# on the same image. A role the release cannot supply falls back to role_fallback, one image
|
||||
# at a time: the rest still install as published.
|
||||
import_release_images() {
|
||||
local role line bundle name digest config target images todo="" needed="" b
|
||||
[ -n "$ARTIFACT_MODE" ] || return 0
|
||||
release_listing || return 0
|
||||
# Read the list whole before matching: the SIGPIPE reason on import_platform_images.
|
||||
images="$(k3s_cmd ctr images ls 2>/dev/null || true)"
|
||||
for role in "$@"; do
|
||||
line="$(awk -v r="$role" '$2 == r' <<<"$RELEASE_LISTING")"
|
||||
if [ -z "$line" ]; then
|
||||
artifact_unusable "the release lists no ${role} image" "$(role_fallback "$role")"
|
||||
continue
|
||||
fi
|
||||
read -r bundle _ name digest config <<<"$line"
|
||||
target="$(role_image "$role")"
|
||||
case "$role" in
|
||||
registry|postgres)
|
||||
# Docker Hub's copy is pinned by digest here; the release's must be that one.
|
||||
if [ "$name" != "$target" ]; then
|
||||
artifact_unusable "the release's ${role} image is ${name}, but this installer runs ${target}" "$(role_fallback "$role")"
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
todo="${todo}${role} ${bundle} ${name} ${target} ${digest} ${config}"$'\n'
|
||||
if ! image_present "$images" "$target" "$digest"; then
|
||||
case "${needed} " in *" ${bundle} "*) ;; *) needed="${needed} ${bundle}" ;; esac
|
||||
fi
|
||||
done
|
||||
for b in $needed; do
|
||||
# artifact_fetch says why it failed; the images the bundle holds are refused below.
|
||||
artifact_fetch "$b" || continue
|
||||
log "importing ${b} into k3s containerd"
|
||||
k3s_cmd ctr images import "$ARTIFACT_FILE" >/dev/null || warn "k3s containerd could not import ${b}"
|
||||
done
|
||||
[ -z "$needed" ] || images="$(k3s_cmd ctr images ls 2>/dev/null || true)"
|
||||
while read -r role bundle name target digest config; do
|
||||
[ -n "$role" ] || continue
|
||||
if ! image_present "$images" "$target" "$digest"; then
|
||||
if ! image_present "$images" "$name" "$digest" \
|
||||
|| ! k3s_cmd ctr images tag --force "$name" "$target" >/dev/null \
|
||||
|| ! k3s_cmd ctr images tag --force "$name" "$(image_repo "$target")@${digest}" >/dev/null; then
|
||||
artifact_unusable "k3s containerd holds no ${target} from ${bundle}" "$(role_fallback "$role")"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
PREBUILT_ROLES="${PREBUILT_ROLES}${role} "
|
||||
RELEASE_IMAGES="${RELEASE_IMAGES}${role} ${bundle} ${name} ${target} ${digest} ${config}"$'\n'
|
||||
# The build each system server runs, for restart_existing_system_servers: the image's
|
||||
# config digest, the id docker gives the same image.
|
||||
case "$role" in
|
||||
limbo) LIMBO_IMAGE_ID="$config" ;;
|
||||
lobby) LOBBY_IMAGE_ID="$config" ;;
|
||||
esac
|
||||
ok "${target} is the release's (${digest:7:12})"
|
||||
done <<<"$todo"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5b. The game stack: the login limbo + lobby images, and the Velocity proxy.
|
||||
#
|
||||
@@ -2608,51 +3082,90 @@ papermc_latest_jar() {
|
||||
}
|
||||
|
||||
build_game_stack() {
|
||||
systemctl start docker
|
||||
local role docker_used=""
|
||||
game_stack_source
|
||||
resolve_game_jars
|
||||
# A release's game images are built from its game-stack.lock, the pinned stack. The latest
|
||||
# stack is resolved on this host at install time, so it is built here.
|
||||
if [ "$FELIS_GAME_STACK" = pinned ]; then
|
||||
import_release_images limbo lobby paper
|
||||
fi
|
||||
for role in limbo lobby paper; do
|
||||
role_prebuilt "$role" && continue
|
||||
[ -n "$docker_used" ] || ensure_docker
|
||||
docker_used=1
|
||||
build_game_image "$role"
|
||||
done
|
||||
install_velocity_plugin
|
||||
stop_docker
|
||||
ok "login + lobby images imported; felis-velocity.jar staged"
|
||||
}
|
||||
|
||||
log "building ${FELIS_LIMBO_IMAGE} (LOOHP/Limbo ${LIMBO_VERSION}, Minecraft ${MC_VERSION})"
|
||||
docker build -f "${GAME_STACK_DIR}/deploy/limbo/Dockerfile" \
|
||||
--build-arg LIMBO_JAR_URL="$LIMBO_JAR_URL" \
|
||||
--build-arg LIMBO_JAR_SHA256="$LIMBO_JAR_SHA256" \
|
||||
--build-arg LIMBO_SCHEM_URL="$LIMBO_SCHEM_URL" \
|
||||
--build-arg LIMBO_SCHEM_SHA256="$LIMBO_SCHEM_SHA256" \
|
||||
--build-arg LIMBO_VERSION="$LIMBO_VERSION" \
|
||||
-t "$FELIS_LIMBO_IMAGE" "$GAME_STACK_DIR"
|
||||
|
||||
log "building ${FELIS_LOBBY_IMAGE} (Paper ${MC_VERSION} + felis-paper /menu + LuckPerms)"
|
||||
docker build -f "${GAME_STACK_DIR}/deploy/lobby/Dockerfile" \
|
||||
--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_SHA256="$LUCKPERMS_JAR_SHA256" \
|
||||
-t "$FELIS_LOBBY_IMAGE" "$GAME_STACK_DIR"
|
||||
|
||||
# Plain Paper, same MC_VERSION and PAPER_JAR_URL (no new dependency). Forwarding is the
|
||||
# operator initContainer's job, so this image carries no /menu plugin and no secret gate.
|
||||
log "building ${FELIS_PAPER_IMAGE} (plain Paper ${MC_VERSION}, forwarding via the operator initContainer)"
|
||||
docker build -f "${GAME_STACK_DIR}/deploy/paper/Dockerfile" \
|
||||
--build-arg PAPER_JAR_URL="$PAPER_JAR_URL" \
|
||||
--build-arg PAPER_JAR_SHA256="$PAPER_JAR_SHA256" \
|
||||
-t "$FELIS_PAPER_IMAGE" "$GAME_STACK_DIR"
|
||||
# build_game_image <role> builds one game image on this host and imports it into k3s containerd.
|
||||
build_game_image() {
|
||||
local img
|
||||
case "$1" in
|
||||
limbo)
|
||||
img="$FELIS_LIMBO_IMAGE"
|
||||
log "building ${img} (LOOHP/Limbo ${LIMBO_VERSION}, Minecraft ${MC_VERSION})"
|
||||
docker build -f "${GAME_STACK_DIR}/deploy/limbo/Dockerfile" \
|
||||
--build-arg LIMBO_JAR_URL="$LIMBO_JAR_URL" \
|
||||
--build-arg LIMBO_JAR_SHA256="$LIMBO_JAR_SHA256" \
|
||||
--build-arg LIMBO_SCHEM_URL="$LIMBO_SCHEM_URL" \
|
||||
--build-arg LIMBO_SCHEM_SHA256="$LIMBO_SCHEM_SHA256" \
|
||||
--build-arg LIMBO_VERSION="$LIMBO_VERSION" \
|
||||
-t "$img" "$GAME_STACK_DIR"
|
||||
;;
|
||||
lobby)
|
||||
img="$FELIS_LOBBY_IMAGE"
|
||||
log "building ${img} (Paper ${MC_VERSION} + felis-paper /menu + LuckPerms)"
|
||||
docker build -f "${GAME_STACK_DIR}/deploy/lobby/Dockerfile" \
|
||||
--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_SHA256="$LUCKPERMS_JAR_SHA256" \
|
||||
-t "$img" "$GAME_STACK_DIR"
|
||||
;;
|
||||
paper)
|
||||
# Plain Paper, same MC_VERSION and PAPER_JAR_URL (no new dependency). Forwarding is the
|
||||
# operator initContainer's job, so this image carries no /menu plugin and no secret gate.
|
||||
img="$FELIS_PAPER_IMAGE"
|
||||
log "building ${img} (plain Paper ${MC_VERSION}, forwarding via the operator initContainer)"
|
||||
docker build -f "${GAME_STACK_DIR}/deploy/paper/Dockerfile" \
|
||||
--build-arg PAPER_JAR_URL="$PAPER_JAR_URL" \
|
||||
--build-arg PAPER_JAR_SHA256="$PAPER_JAR_SHA256" \
|
||||
-t "$img" "$GAME_STACK_DIR"
|
||||
;;
|
||||
esac
|
||||
|
||||
# The builds the system servers run, for restart_existing_system_servers. Docker's layer
|
||||
# cache gives an unchanged build the same id, so a rerun that rebuilt nothing leaves the
|
||||
# login and lobby pods (and every player on them) alone.
|
||||
LIMBO_IMAGE_ID="$(docker image inspect -f '{{.Id}}' "$FELIS_LIMBO_IMAGE")"
|
||||
LOBBY_IMAGE_ID="$(docker image inspect -f '{{.Id}}' "$FELIS_LOBBY_IMAGE")"
|
||||
case "$1" in
|
||||
limbo) LIMBO_IMAGE_ID="$(docker image inspect -f '{{.Id}}' "$img")" ;;
|
||||
lobby) LOBBY_IMAGE_ID="$(docker image inspect -f '{{.Id}}' "$img")" ;;
|
||||
esac
|
||||
|
||||
local img
|
||||
for img in "$FELIS_LIMBO_IMAGE" "$FELIS_LOBBY_IMAGE" "$FELIS_PAPER_IMAGE"; do
|
||||
log "importing ${img} into k3s containerd"
|
||||
remove_k3s_image "$img"
|
||||
docker save "$img" | k3s_cmd ctr images import -
|
||||
done
|
||||
log "importing ${img} into k3s containerd"
|
||||
remove_k3s_image "$img"
|
||||
docker save "$img" | k3s_cmd ctr images import -
|
||||
}
|
||||
|
||||
# install_velocity_plugin puts the release's felis-velocity.jar in place, or builds one here
|
||||
# when the release has none this run can use. The jar is JVM bytecode, one file for every
|
||||
# architecture.
|
||||
install_velocity_plugin() {
|
||||
if [ -n "$ARTIFACT_MODE" ]; then
|
||||
if artifact_fetch felis-velocity.jar; then
|
||||
prepare_velocity_layout
|
||||
install_if_changed "$ARTIFACT_FILE" "${VELOCITY_DIR}/plugins/felis-velocity.jar" 0644 root root
|
||||
ok "felis-velocity.jar is the release's"
|
||||
return 0
|
||||
fi
|
||||
artifact_unusable "the release's felis-velocity.jar cannot be used" "building it on this host instead"
|
||||
fi
|
||||
ensure_docker
|
||||
build_velocity_plugin
|
||||
systemctl stop docker docker.socket 2>/dev/null || true
|
||||
ok "login + lobby images imported; felis-velocity.jar staged"
|
||||
}
|
||||
|
||||
ensure_velocity_directory() {
|
||||
@@ -4469,19 +4982,80 @@ registry_docker_login() {
|
||||
# a re-run: three fast pushes, then "Start request repeated too quickly /
|
||||
# start-limit-hit" and the fourth image never got mirrored. docker.service is
|
||||
# socket-triggered, so each cycle counts twice against the burst limit.
|
||||
#
|
||||
# An image the release shipped (import_release_images) is pushed from its bundle by felis
|
||||
# push-image, and needs no Docker at all; only the images built here go through docker push.
|
||||
push_images_to_registry() {
|
||||
local img
|
||||
systemctl start docker
|
||||
registry_docker_login
|
||||
for img in "$FELIS_IMAGE" "$FELIS_LIMBO_IMAGE" "$FELIS_LOBBY_IMAGE" "$FELIS_PAPER_IMAGE"; do
|
||||
local role img docker_used=""
|
||||
for role in felis limbo lobby paper; do
|
||||
role_prebuilt "$role" || continue
|
||||
push_release_image "$role"
|
||||
done
|
||||
for role in felis limbo lobby paper; do
|
||||
role_prebuilt "$role" && continue
|
||||
img="$(role_image "$role")"
|
||||
[ -n "$img" ] || continue
|
||||
if [ -z "$docker_used" ]; then
|
||||
ensure_docker
|
||||
registry_docker_login
|
||||
docker_used=1
|
||||
fi
|
||||
push_image_to_registry "$img"
|
||||
[ "$role" = felis ] || push_version_tag "$img"
|
||||
done
|
||||
for img in "$FELIS_LIMBO_IMAGE" "$FELIS_LOBBY_IMAGE" "$FELIS_PAPER_IMAGE"; do
|
||||
[ -n "$img" ] || continue
|
||||
push_version_tag "$img"
|
||||
stop_docker
|
||||
# Every bundle is in containerd and the registry now; a rerun that needs one fetches it again.
|
||||
if [ "$ARTIFACT_MODE" = release ]; then
|
||||
rm -rf -- "$ARTIFACT_CACHE"
|
||||
fi
|
||||
}
|
||||
|
||||
# registry_manifest_digest <host/repository:tag> prints the digest the platform registry holds
|
||||
# under that tag, or nothing. Reads are anonymous at the gate.
|
||||
registry_manifest_digest() {
|
||||
local ref="$1" repo tag
|
||||
repo="${ref#*/}"
|
||||
tag="${repo##*:}"
|
||||
repo="${repo%:*}"
|
||||
curl -fsSI --max-time 30 \
|
||||
-H 'Accept: application/vnd.oci.image.manifest.v1+json' \
|
||||
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
|
||||
-H 'Accept: application/vnd.oci.image.index.v1+json' \
|
||||
-H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \
|
||||
"http://${ref%%/*}/v2/${repo}/manifests/${tag}" 2>/dev/null \
|
||||
| tr -d '\r' | awk 'tolower($1) == "docker-content-digest:" { print $2 }' || true
|
||||
}
|
||||
|
||||
# push_release_image <role> mirrors a release image into the platform registry under its tag
|
||||
# and, for a game image, under push_version_tag's <Minecraft version>-<12 hex of its id> as
|
||||
# well, the id being the image's config digest here as it is docker's image id there. A tag
|
||||
# already naming the image's digest is left as it is, so a rerun uploads nothing.
|
||||
push_release_image() {
|
||||
local role="$1" bundle name target digest config refs ref push_ref
|
||||
read -r _ bundle name target digest config <<<"$(awk -v r="$role" '$1 == r' <<<"$RELEASE_IMAGES")"
|
||||
case "$target" in
|
||||
"${REGISTRY_URL}/"*) ;;
|
||||
*)
|
||||
warn "not mirroring ${target} into the internal registry: it is not under ${REGISTRY_URL}; once the image GC collects that tag, nothing can re-pull it"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
refs="$target"
|
||||
if [ "$role" != felis ] && [ -n "${MC_VERSION:-}" ]; then
|
||||
refs="${refs} ${target%:*}:${MC_VERSION}-${config:7:12}"
|
||||
fi
|
||||
for ref in $refs; do
|
||||
push_ref="${REGISTRY_PUSH_HOST}/${ref#"${REGISTRY_URL}/"}"
|
||||
if [ "$(registry_manifest_digest "$push_ref")" = "$digest" ]; then
|
||||
ok "${ref} is already in the internal registry"
|
||||
continue
|
||||
fi
|
||||
artifact_fetch "$bundle" || die "could not mirror ${ref} into the internal registry: ${bundle} (above) is gone"
|
||||
log "mirroring ${ref} into the internal registry"
|
||||
FELIS_REGISTRY_USERNAME=platform FELIS_REGISTRY_PASSWORD="$REGISTRY_PLATFORM_TOKEN" \
|
||||
"$HOST_BIN" push-image --tar "$ARTIFACT_FILE" --image "$name" --ref "$push_ref" >/dev/null \
|
||||
|| die "could not mirror ${ref} into the internal registry — check the registry Deployment/pod (the registry, registry-gate and registry-gc containers) and its PVC"
|
||||
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
|
||||
@@ -4954,30 +5528,37 @@ main() {
|
||||
# do not have: the TUI rebuilds the binary it is already running, and FELIS_SKIP_FETCH
|
||||
# builds whatever is staged, which stamp_version reads the SHA off. Resolving anyway
|
||||
# would set FELIS_VERSION to the newest tag and stamp a staged tree as that release.
|
||||
bootstrap_from_tui || [ -n "${FELIS_SKIP_FETCH:-}" ] || resolve_install_ref
|
||||
# FELIS_ARTIFACT_DIR is its own release: its binary names the version.
|
||||
bootstrap_from_tui || [ -n "${FELIS_SKIP_FETCH:-}" ] || [ -n "$FELIS_ARTIFACT_DIR" ] || resolve_install_ref
|
||||
install_cloudflared
|
||||
load_or_make_secrets
|
||||
configure_offsite
|
||||
ensure_panel_tls_cert
|
||||
install_docker
|
||||
# No install_docker here: Docker comes in only for an image this run has to build
|
||||
# (ensure_docker), and an install from a release's assets builds none.
|
||||
install_k3s
|
||||
# The registry mirror must exist before the bundle's pods start pulling (and
|
||||
# before any re-run's rollouts); the registry's and the database's own images must
|
||||
# be in containerd before their Deployments can start at all.
|
||||
# before any re-run's rollouts).
|
||||
configure_registry_mirror
|
||||
import_platform_images
|
||||
# Three ways to end up with a felis binary, in preference order. The release download is
|
||||
# the only one that skips compiling: it is the CI artifact for this exact tag, panel
|
||||
# included. Both other arms leave HAVE_PREBUILT_BINARY unset where a source build is what
|
||||
# actually happens, which is what routes build_image below.
|
||||
if bootstrap_from_tui; then
|
||||
# Four ways to end up with a felis binary, in preference order. FELIS_ARTIFACT_DIR and the
|
||||
# release download skip compiling: each is the CI artifact for its tag, panel included. The
|
||||
# other arms leave HAVE_PREBUILT_BINARY unset where a source build is what actually
|
||||
# happens, which is what routes build_image below.
|
||||
if [ -n "$FELIS_ARTIFACT_DIR" ]; then
|
||||
install_artifact_binary
|
||||
elif bootstrap_from_tui; then
|
||||
install_embedded_binary
|
||||
elif use_release_binary && download_release_binary; then
|
||||
:
|
||||
else
|
||||
fetch_source
|
||||
fi
|
||||
select_release_artifacts
|
||||
resolve_felis_image
|
||||
# The registry's and the database's own images must be in containerd before their
|
||||
# Deployments can start at all: from the release's bundle when it has them, else pulled.
|
||||
import_release_images felis registry postgres
|
||||
import_platform_images
|
||||
build_image
|
||||
# After build_image imported the felis image: the registry pod's gate runs it.
|
||||
pin_platform_images
|
||||
|
||||
+552
-16
@@ -853,6 +853,7 @@ run_k3s() { # installed-version pinned-version [FELIS_UPGRADE_DEPS]
|
||||
write_k3s_config() { :; }
|
||||
strip_k3s_kubeconfig_mode_flag() { :; }
|
||||
run_k3s_installer() { printf "INSTALLER: %s\n" "$FELIS_K3S_VERSION"; }
|
||||
stage_k3s_airgap_images() { echo STAGE; }
|
||||
systemctl() { :; }
|
||||
wait_for_node_ready() { :; }
|
||||
chmod() { :; }
|
||||
@@ -864,10 +865,15 @@ run_k3s() { # installed-version pinned-version [FELIS_UPGRADE_DEPS]
|
||||
out="$(run_k3s v1.36.4+k3s1 v1.36.4+k3s1 1)"
|
||||
expect "a k3s at the pin is left alone" "OK: k3s v1.36.4+k3s1 already installed" "$out"
|
||||
case "$out" in *INSTALLER:*) echo "FAIL: a k3s at the pin must not be reinstalled"; fails=$((fails + 1)) ;; esac
|
||||
# k3s's image tarball is read as k3s starts, so it is fetched only for a k3s about to start
|
||||
# on a new version: a rerun downloads nothing.
|
||||
case "$out" in *STAGE*) echo "FAIL: a k3s left as it is must not have its images downloaded again"; fails=$((fails + 1)) ;; *) echo "PASS a k3s left as it is stages no images" ;; esac
|
||||
out="$(run_k3s v1.35.2+k3s1 v1.36.4+k3s1)"
|
||||
expect "an older k3s is reported without the flag" "this release pins v1.36.4+k3s1 (FELIS_UPGRADE_DEPS=1 moves it)" "$out"
|
||||
case "$out" in *INSTALLER:*) echo "FAIL: an installed k3s must not move without FELIS_UPGRADE_DEPS=1"; fails=$((fails + 1)) ;; esac
|
||||
expect "FELIS_UPGRADE_DEPS=1 moves k3s up one minor" "INSTALLER: v1.36.4+k3s1" "$(run_k3s v1.35.2+k3s1 v1.36.4+k3s1 1)"
|
||||
expect "an upgrade stages the new k3s's images before its installer restarts it" "STAGE
|
||||
INSTALLER: v1.36.4+k3s1" "$(run_k3s v1.35.2+k3s1 v1.36.4+k3s1 1)"
|
||||
expect "FELIS_UPGRADE_DEPS=1 moves k3s to a newer patch" "INSTALLER: v1.36.4+k3s1" "$(run_k3s v1.36.1+k3s2 v1.36.4+k3s1 1)"
|
||||
out="$(run_k3s v1.34.6+k3s1 v1.36.4+k3s1 1)"
|
||||
expect "a k3s upgrade that skips a minor is refused" "DIE: k3s v1.34.6+k3s1 -> v1.36.4+k3s1 skips a minor version" "$out"
|
||||
@@ -1156,22 +1162,28 @@ rm -rf "$pushdir"
|
||||
|
||||
# docker must be started ONCE for the whole batch: a start/stop pair per image trips
|
||||
# systemd's start rate limit ("start-limit-hit" — observed live; the 4th image was never
|
||||
# mirrored because docker.service is socket-triggered and each cycle counts twice).
|
||||
wiblock="$(awk '/^push_images_to_registry\(\) \{/,/^}/' "$BS")"
|
||||
[ -n "$wiblock" ] || { echo "FAIL: no push_images_to_registry found in $BS"; exit 1; }
|
||||
out="$(
|
||||
FELIS_IMAGE=a FELIS_LIMBO_IMAGE=b FELIS_LOBBY_IMAGE=c FELIS_PAPER_IMAGE=d bash -c '
|
||||
# mirrored because docker.service is socket-triggered and each cycle counts twice). An image
|
||||
# the release shipped is pushed from its bundle instead, and needs no Docker at all.
|
||||
wiblock="$(for f in push_images_to_registry role_prebuilt role_image stop_docker; do awk '/^'"$f"'\(\) \{/,/^}/' "$BS"; done)"
|
||||
case "$wiblock" in *"push_images_to_registry() {"*"role_prebuilt() {"*"role_image() {"*"stop_docker() {"*) ;; *) echo "FAIL: push_images_to_registry or its helpers are missing from $BS"; exit 1 ;; esac
|
||||
run_batch() { # PREBUILT_ROLES [ARTIFACT_MODE [ARTIFACT_CACHE]]
|
||||
FELIS_IMAGE=a FELIS_LIMBO_IMAGE=b FELIS_LOBBY_IMAGE=c FELIS_PAPER_IMAGE=d PREBUILT_ROLES="$1" \
|
||||
ARTIFACT_MODE="${2:-}" ARTIFACT_CACHE="${3:-/nonexistent}" bash -c '
|
||||
DOCKER_INSTALLED=""
|
||||
systemctl() { printf "SYSTEMCTL %s\n" "$*"; }
|
||||
ensure_docker() { printf "ENSURE\n"; DOCKER_INSTALLED=1; }
|
||||
push_image_to_registry() { printf "PUSH %s\n" "$1"; }
|
||||
push_version_tag() { printf "VERSION %s\n" "$1"; }
|
||||
push_release_image() { printf "RELEASE %s\n" "$1"; }
|
||||
registry_docker_login() { printf "LOGIN\n"; }
|
||||
'"$wiblock"'
|
||||
push_images_to_registry'
|
||||
)"
|
||||
expect "the batch logs in to the registry gate before pushing" "SYSTEMCTL start docker
|
||||
}
|
||||
out="$(run_batch " ")"
|
||||
expect "the batch logs in to the registry gate before pushing" "ENSURE
|
||||
LOGIN
|
||||
PUSH a" "$out"
|
||||
starts="$(printf '%s\n' "$out" | grep -c 'SYSTEMCTL start docker')"
|
||||
starts="$(printf '%s\n' "$out" | grep -c 'ENSURE')"
|
||||
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" \
|
||||
@@ -1179,6 +1191,24 @@ stops="$(printf '%s\n' "$out" | grep -c 'SYSTEMCTL stop docker')"
|
||||
[ "$(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)); }
|
||||
out="$(run_batch " felis limbo ")"
|
||||
expect "release images are pushed from their bundles, the rest with docker" "RELEASE felis
|
||||
RELEASE limbo
|
||||
ENSURE
|
||||
LOGIN
|
||||
PUSH c
|
||||
VERSION c
|
||||
PUSH d
|
||||
VERSION d
|
||||
SYSTEMCTL stop docker docker.socket" "$out"
|
||||
case "$out" in *"PUSH a"*|*"PUSH b"*) echo "FAIL: a release image must not also go through docker push"; fails=$((fails + 1)) ;; *) echo "PASS a release image is pushed once" ;; esac
|
||||
cachedir="$(mktemp -d)"
|
||||
out="$(run_batch " felis limbo lobby paper " release "$cachedir")"
|
||||
[ "$(printf '%s\n' "$out" | grep -c '^RELEASE')" = 4 ] && [ "$(printf '%s\n' "$out" | grep -c '^RELEASE')" = "$(printf '%s\n' "$out" | wc -l | tr -d ' ')" ] \
|
||||
&& echo "PASS an install from release images pushes without Docker, and never starts or stops it" \
|
||||
|| { echo "FAIL: an all-release batch touched docker:"; printf '%s\n' "$out"; fails=$((fails + 1)); }
|
||||
[ ! -e "$cachedir" ] && echo "PASS the downloaded bundles are removed once they are pushed" \
|
||||
|| { echo "FAIL: ${cachedir} was left behind after the pushes"; fails=$((fails + 1)); rm -rf "$cachedir"; }
|
||||
|
||||
# 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.
|
||||
@@ -2082,12 +2112,16 @@ case "$out" in
|
||||
*) echo "FAIL restart_existing_control_plane died on a first install: $out"; fails=$((fails + 1)) ;;
|
||||
esac
|
||||
|
||||
mainblock="$(awk '/^main\(\) \{/,/^}/' "$BS")"
|
||||
case "$mainblock" in
|
||||
*"resolve_felis_image
|
||||
build_image"*) echo "PASS the image is named before it is built" ;;
|
||||
*) echo "FAIL main must call resolve_felis_image right before build_image"; fails=$((fails + 1)) ;;
|
||||
esac
|
||||
# The binary names the release, the release names the images, and each image is looked for in
|
||||
# the release's bundles before anything is pulled or built.
|
||||
imorder="$(awk '/^main\(\) \{/,/^}/' "$BS" | grep -nE '^[[:space:]]*(install_k3s|configure_registry_mirror|install_artifact_binary|fetch_source|select_release_artifacts|resolve_felis_image|import_release_images felis registry postgres|import_platform_images|build_image|pin_platform_images)$' | sed 's/^[0-9]*:[[:space:]]*//' | tr '\n' ' ')"
|
||||
expect "main takes the binary, then the release's images, before pulling or building any" \
|
||||
"install_k3s configure_registry_mirror install_artifact_binary fetch_source select_release_artifacts resolve_felis_image import_release_images felis registry postgres import_platform_images build_image pin_platform_images " "$imorder"
|
||||
if awk '/^main\(\) \{/,/^}/' "$BS" | grep -qE '^[[:space:]]*install_docker$'; then
|
||||
echo "FAIL: main installs Docker up front; only a build may (ensure_docker)"; fails=$((fails + 1))
|
||||
else
|
||||
echo "PASS main leaves Docker to the builds that need it"
|
||||
fi
|
||||
|
||||
# --- a rerun restarts only what changed -------------------------------------------------
|
||||
# The proxy, the login and lobby pods and PostgreSQL each disconnect every player (or cut
|
||||
@@ -2488,6 +2522,7 @@ run_install_k3s() { # $1: installed version ("" = none), $2: drop-in changed (0|
|
||||
write_k3s_config() { [ "$CHANGED" = 0 ] || K3S_RESTART_NEEDED=1; }
|
||||
strip_k3s_kubeconfig_mode_flag() { :; }
|
||||
run_k3s_installer() { echo "INSTALLER"; printf "#!/bin/sh\n" > "$K3S_BIN"; command chmod +x "$K3S_BIN"; }
|
||||
stage_k3s_airgap_images() { echo "STAGE"; }
|
||||
systemctl() { echo "SYSTEMCTL: $*"; }
|
||||
wait_for_node_ready() { echo "READY"; }
|
||||
chmod() { echo "CHMOD: $*"; }
|
||||
@@ -2503,6 +2538,8 @@ case "$out" in *"restart k3s"*) echo "FAIL unchanged k3s settings must not resta
|
||||
out="$(run_install_k3s "" 1)"
|
||||
expect "a fresh host runs the k3s installer and waits for the node" "INSTALLER
|
||||
SYSTEMCTL: enable --now k3s" "$out"
|
||||
expect "a fresh host stages k3s's images before k3s first starts" "STAGE
|
||||
INSTALLER" "$out"
|
||||
expect "a fresh host's kubeconfig is made root-only too" "CHMOD: 0600 /etc/rancher/k3s/k3s.yaml" "$out"
|
||||
case "$out" in *"restart k3s"*) echo "FAIL the k3s installer already started k3s on the new settings; no second restart"; fails=$((fails + 1)) ;; *) echo "PASS a fresh k3s is not restarted a second time" ;; esac
|
||||
out="$(run_install_k3s v1.35.2+k3s1 1 1)"
|
||||
@@ -2674,7 +2711,8 @@ wdblock="$(awk '/^warn_dynamic_node_ip\(\) \{/,/^}/' "$BS")"
|
||||
pfroot="$(mktemp -d)"
|
||||
# run_pf runs preflight with the stubbed facts in the environment; each defaults to a
|
||||
# healthy host: 8 GiB RAM, one 100 GiB filesystem with 60 GiB free, no listeners, no
|
||||
# other cluster, a LAN route, every download host answering.
|
||||
# other cluster, a LAN route, every download host answering. The install defaults to a
|
||||
# source build (the dev channel), the one that writes the most and downloads from the most.
|
||||
run_pf() {
|
||||
PF_ROOT="$pfroot" bash -c '
|
||||
set -Eeuo pipefail
|
||||
@@ -2713,11 +2751,14 @@ run_pf() {
|
||||
host="${host#https://}"; host="${host%/}"
|
||||
case " ${PF_DOWN:-} " in *" ${host} "*) echo 000 ;; *) echo 404 ;; esac
|
||||
}
|
||||
bootstrap_from_tui() { return 1; }
|
||||
bootstrap_from_tui() { [ -n "${PF_TUI:-}" ]; }
|
||||
'"$(awk '/^use_release_binary\(\) \{/,/^}/' "$BS")"'
|
||||
FELIS_GAME_PORT=25565 FELIS_PANEL_NODEPORT=30443 REGISTRY_URL=registry.felis.svc:5000 PG_HOST_PORT=15432
|
||||
POD_CIDR=10.42.0.0/16 SERVICE_CIDR=10.43.0.0/16 NODE_IP="${PF_NODE_IP:-192.168.1.20}"
|
||||
K3S_BIN="$PF_ROOT/k3s" BOOTSTRAP_DONE="$PF_ROOT/bootstrap.done"
|
||||
FELIS_REF_PINNED="" FELIS_VELOCITY_FORK_JAR="" FELIS_PREFLIGHT="${PF_MODE:-strict}"
|
||||
FELIS_VERSION_BOOTSTRAP="${PF_CHANNEL:-dev}" FELIS_ARTIFACT_DIR="${PF_ARTIFACT_DIR:-}"
|
||||
FELIS_GAME_STACK="${PF_GAME_STACK:-pinned}"
|
||||
'"$wdblock"'
|
||||
'"$pfblock"'
|
||||
# The host readers the section defines, answered from the same facts.
|
||||
@@ -2794,6 +2835,31 @@ expect "and the install goes on" "WENT ON" "$out"
|
||||
out="$(PF_DOWN="github.com fill-data.papermc.io" run_pf)"
|
||||
expect "unreachable download hosts are named together" "cannot reach github.com fill-data.papermc.io over HTTPS" "$out"
|
||||
|
||||
# An install from a release's assets builds nothing: no Docker cache under /var/lib/containerd,
|
||||
# and Docker Hub only as the fallback for an image the release cannot supply. The downloaded
|
||||
# bundles wait under /var/lib/felis until they are pushed, so that budget grows.
|
||||
out="$(PF_CHANNEL=release PF_DF="/ 104857600 83886080 20971520" run_pf)"
|
||||
expect "a release install fits where a source build does not" "OK: preflight passed" "$out"
|
||||
out="$(PF_CHANNEL=release PF_DF="/ 104857600 88080384 16777216" run_pf)"
|
||||
expect "a release install still needs room for its bundles" "/ has 16384 MiB free; this install writes about 17408 MiB there" "$out"
|
||||
out="$(PF_ARTIFACT_DIR=/srv/felis-release PF_DF="/ 104857600 88080384 16777216" PF_DOWN="api.github.com registry-1.docker.io" run_pf)"
|
||||
expect "an install from FELIS_ARTIFACT_DIR downloads no bundles and asks neither GitHub's API nor Docker Hub" "OK: preflight passed" "$out"
|
||||
case "$out" in *"cannot reach"*) echo "FAIL: FELIS_ARTIFACT_DIR probed a host it never uses"; fails=$((fails + 1)) ;; *) echo "PASS FELIS_ARTIFACT_DIR probes only what it uses" ;; esac
|
||||
out="$(PF_CHANNEL=release PF_DOWN=registry-1.docker.io run_pf)"
|
||||
expect "a release install without Docker Hub is warned about" "WARN: preflight: cannot reach registry-1.docker.io over HTTPS; the install goes on" "$out"
|
||||
expect "and goes on" "OK: preflight passed" "$out"
|
||||
out="$(PF_DOWN=registry-1.docker.io run_pf)"
|
||||
expect "a source build without Docker Hub is refused" "cannot reach registry-1.docker.io over HTTPS; the install downloads from there" "$out"
|
||||
out="$(PF_CHANNEL=release PF_GAME_STACK=latest PF_DOWN=registry-1.docker.io run_pf)"
|
||||
expect "FELIS_GAME_STACK=latest builds its images here, so it needs Docker Hub" "cannot reach registry-1.docker.io over HTTPS; the install downloads from there" "$out"
|
||||
out="$(PF_CHANNEL=release PF_GAME_STACK=latest PF_DF="/ 104857600 83886080 20971520" run_pf)"
|
||||
expect "and room for the builds" "/ has 20480 MiB free; this install writes about 25600 MiB there" "$out"
|
||||
out="$(PF_CHANNEL=release PF_DOWN=api.github.com run_pf)"
|
||||
expect "the release channel cannot install without GitHub's API" "cannot reach api.github.com over HTTPS; the install downloads from there" "$out"
|
||||
out="$(PF_TUI=1 PF_DOWN=api.github.com run_pf)"
|
||||
expect "the setup console only warns without it: its binary is already here" "WARN: preflight: cannot reach api.github.com" "$out"
|
||||
expect "and goes on" "OK: preflight passed" "$out"
|
||||
|
||||
# Three problems, one report, nothing done.
|
||||
out="$(PF_MEM_KB=1000000 PF_ARCH=armv7l PF_DOWN=github.com run_pf)"
|
||||
expect "every problem is in the one report" "preflight found 3 problem(s); nothing on this host has been changed" "$out"
|
||||
@@ -3263,6 +3329,476 @@ before "the control plane is scaled back up before the rollouts are awaited" \
|
||||
'kube -n "$CONTROL_NS" scale deployment felis-api felis-operator --replicas=1' 'rollout status "$d"' "$dbblock"
|
||||
expect "the pods reach the database at its Service" 'write_felis_toml "${STATE_DIR}/felis.pod.toml" "$PG_SERVICE_ADDR"' "$dbblock"
|
||||
|
||||
# --- a release's prebuilt assets ----------------------------------------------------------
|
||||
# Every file is used only once its sha256 is the one SHA256SUMS lists; the interesting cases
|
||||
# are the refusals, and that a refused file is never handed on.
|
||||
for f in load_artifact_sums artifact_sum artifact_fetch artifact_unusable install_artifact_binary \
|
||||
select_release_artifacts load_release_listing release_listing import_release_images \
|
||||
push_release_image registry_manifest_digest stage_k3s_airgap_images install_velocity_plugin \
|
||||
build_game_stack load_release_json; do
|
||||
[ -n "$(bsfn "$f")" ] || { echo "FAIL: no ${f} in $BS"; exit 1; }
|
||||
[ "$(bsfn "$f" | wc -l)" -lt 90 ] \
|
||||
|| { echo "FAIL: the extracted ${f} is not just the function -- did its closing brace move?"; exit 1; }
|
||||
done
|
||||
adir="$(mktemp -d)"
|
||||
sha() { sha256sum <"$1" | cut -d' ' -f1; }
|
||||
afblock="$(bsfn artifact_sum; bsfn artifact_fetch)"
|
||||
# run_fetch_artifact <mode> <name> [sums]: the directory is $adir/dir, the cache $adir/cache,
|
||||
# and the "release" serves $adir/release/<name>.
|
||||
run_fetch_artifact() {
|
||||
MODE="$1" NAME="$2" SUMS="${3:-$(cat "$adir/SUMS")}" A="$adir" bash -c '
|
||||
set -Eeuo pipefail
|
||||
warn() { echo "WARN: $*"; }
|
||||
download_release_asset() { echo "DOWNLOAD $2" >&2; [ -f "$A/release/$2" ] || return 1; cp "$A/release/$2" "$3"; }
|
||||
ARTIFACT_MODE="$MODE" ARTIFACT_TAG=v9.9.9 ARTIFACT_SUMS="$SUMS" ARTIFACT_CACHE="$A/cache"
|
||||
FELIS_ARTIFACT_DIR="$A/dir"
|
||||
'"$afblock"'
|
||||
if artifact_fetch "$NAME"; then echo "FILE $ARTIFACT_FILE"; cat "$ARTIFACT_FILE"; else echo REFUSED; fi' 2>&1
|
||||
}
|
||||
mkdir -p "$adir/dir" "$adir/release"
|
||||
printf 'the game images\n' > "$adir/dir/felis-image-game-linux-amd64.tar"
|
||||
cp "$adir/dir/felis-image-game-linux-amd64.tar" "$adir/release/"
|
||||
printf '%s felis-image-game-linux-amd64.tar\n%s *felis-velocity.jar\n' \
|
||||
"$(sha "$adir/dir/felis-image-game-linux-amd64.tar")" "$(printf 'the plugin\n' | sha256sum | cut -d' ' -f1)" > "$adir/SUMS"
|
||||
out="$(run_fetch_artifact dir felis-image-game-linux-amd64.tar)"
|
||||
expect "a directory's file that matches SHA256SUMS is used where it is" "FILE $adir/dir/felis-image-game-linux-amd64.tar" "$out"
|
||||
out="$(run_fetch_artifact dir felis-velocity.jar)"
|
||||
expect "a file the directory lacks is refused" "WARN: $adir/dir/felis-velocity.jar is missing" "$out"
|
||||
case "$out" in *DOWNLOAD*) echo "FAIL: FELIS_ARTIFACT_DIR went to the release for a file it lacks"; fails=$((fails + 1)) ;; *) echo "PASS FELIS_ARTIFACT_DIR never downloads" ;; esac
|
||||
printf 'not the plugin\n' > "$adir/dir/felis-velocity.jar"
|
||||
out="$(run_fetch_artifact dir felis-velocity.jar)"
|
||||
expect "a directory's file that does not match is refused, naming both hashes" "hashes to $(sha "$adir/dir/felis-velocity.jar"), but SHA256SUMS says" "$out"
|
||||
case "$out" in *FILE*) echo "FAIL: a mismatched file was handed on"; fails=$((fails + 1)) ;; *) echo "PASS a mismatched file is never handed on" ;; esac
|
||||
out="$(run_fetch_artifact dir felis-linux-amd64)"
|
||||
expect "a name SHA256SUMS does not list is refused" "WARN: SHA256SUMS lists no felis-linux-amd64" "$out"
|
||||
out="$(run_fetch_artifact release felis-linux-amd64)"
|
||||
case "$out" in *DOWNLOAD*|*FILE*) echo "FAIL: a file SHA256SUMS does not list was fetched: $out"; fails=$((fails + 1)) ;; *) echo "PASS a file SHA256SUMS does not list is never downloaded" ;; esac
|
||||
out="$(run_fetch_artifact dir felis-image-game-linux-amd64.tar "$(printf 'deadbeef felis-image-game-linux-amd64.tar\n')")"
|
||||
expect "a SHA256SUMS line without a whole sha256 lists nothing" "WARN: SHA256SUMS lists no felis-image-game-linux-amd64.tar" "$out"
|
||||
|
||||
out="$(run_fetch_artifact release felis-image-game-linux-amd64.tar)"
|
||||
expect "a release file is downloaded into the cache once it matches" "FILE $adir/cache/felis-image-game-linux-amd64.tar" "$out"
|
||||
[ ! -e "$adir/cache/felis-image-game-linux-amd64.tar.partial" ] && echo "PASS the download's partial file is gone" \
|
||||
|| { echo "FAIL: the partial download was left behind"; fails=$((fails + 1)); }
|
||||
[ "$(stat -c %a "$adir/cache" 2>/dev/null || stat -f %Lp "$adir/cache")" = 700 ] && echo "PASS the cache is root's alone" \
|
||||
|| { echo "FAIL: the artifact cache is not 0700"; fails=$((fails + 1)); }
|
||||
out="$(run_fetch_artifact release felis-image-game-linux-amd64.tar)"
|
||||
case "$out" in *DOWNLOAD*) echo "FAIL: a cached file that still matches was downloaded again"; fails=$((fails + 1)) ;; *) echo "PASS a cached file that still matches is reused" ;; esac
|
||||
printf 'tampered\n' > "$adir/cache/felis-image-game-linux-amd64.tar"
|
||||
out="$(run_fetch_artifact release felis-image-game-linux-amd64.tar)"
|
||||
expect "a cached file that no longer matches is fetched again" "DOWNLOAD felis-image-game-linux-amd64.tar" "$out"
|
||||
expect "and the fresh copy is used" "the game images" "$out"
|
||||
printf 'a swapped asset\n' > "$adir/release/felis-velocity.jar"
|
||||
out="$(run_fetch_artifact release felis-velocity.jar)"
|
||||
expect "a download that does not match is refused" "downloaded felis-velocity.jar hashes to $(sha "$adir/release/felis-velocity.jar"), but release v9.9.9's SHA256SUMS says" "$out"
|
||||
[ ! -e "$adir/cache/felis-velocity.jar" ] && [ ! -e "$adir/cache/felis-velocity.jar.partial" ] \
|
||||
&& echo "PASS a refused download leaves nothing in the cache" \
|
||||
|| { echo "FAIL: a refused download was kept"; fails=$((fails + 1)); }
|
||||
rm -f "$adir/release/felis-velocity.jar"
|
||||
out="$(run_fetch_artifact release felis-velocity.jar)"
|
||||
expect "a release without the asset is refused" "could not download felis-velocity.jar from release v9.9.9" "$out"
|
||||
[ "$(printf '%s\n' "$out" | grep -c '^WARN')" = 1 ] && echo "PASS a failed download is reported once, as a failed download" \
|
||||
|| { echo "FAIL: a failed download went on to be checked: $out"; fails=$((fails + 1)); }
|
||||
|
||||
# From FELIS_ARTIFACT_DIR nothing is ever built, so an unusable asset stops the install; from
|
||||
# a release it falls back, with a warning.
|
||||
unblock="$(bsfn artifact_unusable)"
|
||||
out="$(ARTIFACT_MODE=dir bash -c 'die() { echo "DIE: $*"; exit 1; }; warn() { echo "WARN: $*"; }; '"$unblock"'; artifact_unusable "no plugin" "building it"; echo GOES ON')"
|
||||
expect "FELIS_ARTIFACT_DIR stops on an unusable asset" "DIE: FELIS_ARTIFACT_DIR: no plugin" "$out"
|
||||
case "$out" in *"GOES ON"*) echo "FAIL: FELIS_ARTIFACT_DIR went on without an asset"; fails=$((fails + 1)) ;; esac
|
||||
out="$(ARTIFACT_MODE=release bash -c 'die() { echo "DIE: $*"; exit 1; }; warn() { echo "WARN: $*"; }; '"$unblock"'; artifact_unusable "no plugin" "building it"; echo GOES ON')"
|
||||
expect "a release's unusable asset falls back" "WARN: no plugin; building it" "$out"
|
||||
expect "and the install goes on" "GOES ON" "$out"
|
||||
|
||||
# --- the image listing ----------------------------------------------------------------------
|
||||
llblock="$(bsfn load_release_listing)"
|
||||
d1="sha256:$(printf 'm' | sha256sum | cut -d' ' -f1)"
|
||||
d2="sha256:$(printf 'c' | sha256sum | cut -d' ' -f1)"
|
||||
good="felis-image-game-linux-amd64.tar limbo registry.felis.svc:5000/felis/limbo:demo $d1 $d2
|
||||
felis-image-base-linux-amd64.tar registry docker.io/library/registry@$d1 $d1 $d2"
|
||||
run_listing() { # content
|
||||
printf '%s\n' "$1" > "$adir/listing"
|
||||
bash -c "$llblock"'
|
||||
if load_release_listing "$0" amd64; then printf "%s" "$RELEASE_LISTING"; echo LOADED; else echo REFUSED; fi' "$adir/listing"
|
||||
}
|
||||
expect "a well-formed listing is loaded" "LOADED" "$(run_listing "$good")"
|
||||
expect "a listing for another architecture is refused" "REFUSED" "$(run_listing "$(printf '%s\n' "$good" | sed 's/amd64/arm64/')")"
|
||||
expect "a line with a sixth field is refused" "REFUSED" "$(run_listing "$good extra")"
|
||||
expect "a line missing its config digest is refused" "REFUSED" "$(run_listing "felis-image-game-linux-amd64.tar limbo registry.felis.svc:5000/felis/limbo:demo $d1")"
|
||||
expect "a role Felis does not know is refused" "REFUSED" "$(run_listing "felis-image-game-linux-amd64.tar miner x/y:z $d1 $d2")"
|
||||
expect "a role listed twice is refused" "REFUSED" "$(run_listing "$good
|
||||
felis-image-game-linux-amd64.tar limbo other/limbo:demo $d1 $d2")"
|
||||
expect "a short digest is refused" "REFUSED" "$(run_listing "felis-image-game-linux-amd64.tar limbo x/limbo:demo sha256:abc $d2")"
|
||||
expect "a name with shell characters is refused" "REFUSED" "$(run_listing "felis-image-game-linux-amd64.tar limbo x/limbo:\$(id) $d1 $d2")"
|
||||
expect "a bundle outside the felis-image-* names is refused" "REFUSED" "$(run_listing "../../etc/shadow limbo x/limbo:demo $d1 $d2")"
|
||||
expect "an empty listing is refused" "REFUSED" "$(run_listing "")"
|
||||
|
||||
# --- importing a release's images into containerd --------------------------------------------
|
||||
# ctr is a fake containerd: `images ls` prints the refs in $adir/ctr (with the header row the
|
||||
# real one has), `images import` adds what the imported bundle's .names file holds, and
|
||||
# `images tag` adds a name.
|
||||
ilblock="$(bsfn artifact_unusable; bsfn role_image; bsfn role_fallback; bsfn role_prebuilt; bsfn image_repo; bsfn image_present; bsfn pinned_image_ref; bsfn import_release_images)"
|
||||
fdig="sha256:$(printf 'felis' | sha256sum | cut -d' ' -f1)"
|
||||
gdig="sha256:$(printf 'limbo' | sha256sum | cut -d' ' -f1)"
|
||||
rdig="sha256:$(printf 'registry' | sha256sum | cut -d' ' -f1)"
|
||||
cdig="sha256:0123456789ab$(printf 'cfg' | sha256sum | cut -c13-64)"
|
||||
ridx="sha256:$(printf 'index' | sha256sum | cut -d' ' -f1)"
|
||||
listing="felis-image-felis-linux-amd64.tar felis registry.felis.svc:5000/felis/felis:v9.9.9 $fdig $cdig
|
||||
felis-image-game-linux-amd64.tar limbo registry.felis.svc:5000/felis/limbo:demo $gdig $cdig
|
||||
felis-image-base-linux-amd64.tar registry docker.io/library/registry@$ridx $rdig $cdig"
|
||||
run_import_release() { # mode ctr-refs FELIS_IMAGE-or-empty roles...
|
||||
ir_mode="$1" ir_refs="$2" ir_fimg="${3:-registry.felis.svc:5000/felis/felis:v9.9.9}"
|
||||
shift 3
|
||||
MODE="$ir_mode" REFS="$ir_refs" FIMG="$ir_fimg" A="$adir" LISTING="$listing" \
|
||||
FD="$fdig" GD="$gdig" RD="$rdig" RIDX="$ridx" bash -c '
|
||||
set -Eeuo pipefail
|
||||
die() { echo "DIE: $*"; exit 1; }
|
||||
warn() { echo "WARN: $*"; }
|
||||
log() { :; }
|
||||
ok() { echo "OK: $*"; }
|
||||
printf "%s\n" "$REFS" > "$A/ctr"
|
||||
k3s_cmd() {
|
||||
shift
|
||||
case "$2" in
|
||||
ls) echo "REF TYPE DIGEST SIZE PLATFORMS LABELS"; awk "NF { print \$1, \"application/vnd.oci.image.manifest.v1+json\", \$2, \"25.3 MiB\", \"linux/amd64\", \"-\" }" "$A/ctr" ;;
|
||||
import) echo "IMPORT ${3##*/}" >&2; cat "$3.names" >> "$A/ctr" ;;
|
||||
tag) echo "TAG $4 $5" >&2; awk -v s="$4" -v t="$5" "\$1 == s { print t, \$2 }" "$A/ctr" >> "$A/ctr" ;;
|
||||
esac
|
||||
}
|
||||
artifact_fetch() { echo "FETCH $1" >&2; ARTIFACT_FILE="$A/bundles/$1"; [ -f "$ARTIFACT_FILE" ]; }
|
||||
release_listing() { RELEASE_LISTING="$LISTING"; }
|
||||
ARTIFACT_MODE="$MODE" PREBUILT_ROLES=" " RELEASE_IMAGES="" LIMBO_IMAGE_ID="" LOBBY_IMAGE_ID=""
|
||||
FELIS_IMAGE="$FIMG" FELIS_LIMBO_IMAGE=registry.felis.svc:5000/felis/limbo:demo
|
||||
FELIS_LOBBY_IMAGE=registry.felis.svc:5000/felis/lobby:demo FELIS_PAPER_IMAGE=registry.felis.svc:5000/felis/paper:demo
|
||||
REGISTRY_IMAGE="docker.io/library/registry:2.8.3@$RIDX" POSTGRES_IMAGE="docker.io/library/postgres:18.6-trixie@$RIDX"
|
||||
'"$ilblock"'
|
||||
import_release_images "$@"
|
||||
echo "PREBUILT[$PREBUILT_ROLES] LIMBO_ID[$LIMBO_IMAGE_ID]"
|
||||
printf "%s" "$RELEASE_IMAGES"' x "$@" 2>&1
|
||||
}
|
||||
mkdir -p "$adir/bundles"
|
||||
: > "$adir/bundles/felis-image-felis-linux-amd64.tar"
|
||||
printf '%s %s\n' "registry.felis.svc:5000/felis/felis:v9.9.9" "$fdig" "registry.felis.svc:5000/felis/felis@$fdig" "$fdig" \
|
||||
> "$adir/bundles/felis-image-felis-linux-amd64.tar.names"
|
||||
: > "$adir/bundles/felis-image-game-linux-amd64.tar"
|
||||
printf '%s %s\n' "registry.felis.svc:5000/felis/limbo:demo" "$gdig" > "$adir/bundles/felis-image-game-linux-amd64.tar.names"
|
||||
: > "$adir/bundles/felis-image-base-linux-amd64.tar"
|
||||
printf '%s %s\n' "docker.io/library/registry@$ridx" "$rdig" > "$adir/bundles/felis-image-base-linux-amd64.tar.names"
|
||||
|
||||
out="$(run_import_release release "" "" felis registry)"
|
||||
expect "a fresh node imports the bundles holding the images it lacks" "IMPORT felis-image-felis-linux-amd64.tar" "$out"
|
||||
expect "the base bundle too" "IMPORT felis-image-base-linux-amd64.tar" "$out"
|
||||
case "$out" in *"felis-image-game"*) echo "FAIL: a bundle no asked-for image is in was fetched"; fails=$((fails + 1)) ;; *) echo "PASS only the bundles holding the asked-for images are fetched" ;; esac
|
||||
expect "each imported image is recorded as the release's" "PREBUILT[ felis registry ]" "$out"
|
||||
expect "with the line push_release_image reads" "felis felis-image-felis-linux-amd64.tar registry.felis.svc:5000/felis/felis:v9.9.9 registry.felis.svc:5000/felis/felis:v9.9.9 $fdig $cdig" "$out"
|
||||
|
||||
out="$(run_import_release release "registry.felis.svc:5000/felis/felis:v9.9.9 $fdig
|
||||
docker.io/library/registry@$ridx $ridx" "" felis registry)"
|
||||
case "$out" in *FETCH*|*IMPORT*) echo "FAIL: images containerd already holds were fetched again"; fails=$((fails + 1)) ;; *) echo "PASS a rerun fetches nothing containerd already holds" ;; esac
|
||||
expect "a digest name CRI pulled counts as the image" "PREBUILT[ felis registry ]" "$out"
|
||||
|
||||
out="$(run_import_release release "registry.felis.svc:5000/felis/felis:v9.9.9 sha256:$(printf old | sha256sum | cut -d' ' -f1)" "" felis)"
|
||||
expect "a tag naming another build is imported over" "IMPORT felis-image-felis-linux-amd64.tar" "$out"
|
||||
|
||||
out="$(run_import_release release "" "registry.felis.svc:5000/felis/felis:custom" felis)"
|
||||
expect "a FELIS_IMAGE set to another name gets that name on the release's image" "TAG registry.felis.svc:5000/felis/felis:v9.9.9 registry.felis.svc:5000/felis/felis:custom" "$out"
|
||||
expect "and its digest name, for a pinned pull" "TAG registry.felis.svc:5000/felis/felis:v9.9.9 registry.felis.svc:5000/felis/felis@$fdig" "$out"
|
||||
expect "and counts as the release's" "PREBUILT[ felis ]" "$out"
|
||||
|
||||
out="$(run_import_release release "" "" limbo)"
|
||||
expect "a game image's config digest is the build the login server runs" "LIMBO_ID[$cdig]" "$out"
|
||||
|
||||
out="$(run_import_release release "" "" paper)"
|
||||
expect "a role the release does not list falls back alone" "WARN: the release lists no paper image; building it on this host instead" "$out"
|
||||
expect "and is not recorded as the release's" "PREBUILT[ ]" "$out"
|
||||
[ "$(printf '%s\n' "$out" | grep -c '^WARN')" = 1 ] && echo "PASS a role the release does not list is reported once" \
|
||||
|| { echo "FAIL: a role the release does not list went on to be looked for in containerd: $out"; fails=$((fails + 1)); }
|
||||
out="$(run_import_release dir "" "" paper)"
|
||||
expect "from FELIS_ARTIFACT_DIR a missing role stops the install" "DIE: FELIS_ARTIFACT_DIR: the release lists no paper image" "$out"
|
||||
|
||||
out="$(run_import_release release "" "" postgres)"
|
||||
expect "a base image the release does not list falls back to Docker Hub" "WARN: the release lists no postgres image; k3s pulls it from Docker Hub instead" "$out"
|
||||
listing="$(printf '%s\n' "$listing" | sed "s|docker.io/library/registry@$ridx|docker.io/library/registry@sha256:$(printf other | sha256sum | cut -d' ' -f1)|")"
|
||||
out="$(run_import_release release "" "" registry)"
|
||||
expect "the release's registry must be the one this installer pins" "the release's registry image is docker.io/library/registry@sha256:$(printf other | sha256sum | cut -d' ' -f1), but this installer runs docker.io/library/registry@$ridx" "$out"
|
||||
expect "and falls back to Docker Hub" "k3s pulls it from Docker Hub instead" "$out"
|
||||
case "$out" in *IMPORT*) echo "FAIL: a refused base image was imported"; fails=$((fails + 1)) ;; *) echo "PASS a refused base image is not imported" ;; esac
|
||||
|
||||
rm -f "$adir/bundles/felis-image-game-linux-amd64.tar.names"
|
||||
: > "$adir/bundles/felis-image-game-linux-amd64.tar.names"
|
||||
out="$(run_import_release release "" "" limbo)"
|
||||
expect "a bundle that does not hold the image it is listed for falls back" "WARN: k3s containerd holds no registry.felis.svc:5000/felis/limbo:demo from felis-image-game-linux-amd64.tar; building it on this host instead" "$out"
|
||||
expect "and records nothing" "PREBUILT[ ]" "$out"
|
||||
|
||||
out="$(run_import_release "" "" "" felis limbo)"
|
||||
case "$out" in *FETCH*|*IMPORT*|*WARN*) echo "FAIL: a source build looked at release bundles"; fails=$((fails + 1)) ;; *) echo "PASS a source build imports nothing from a release" ;; esac
|
||||
|
||||
# The listing is read once per run, and a bad one is reported once.
|
||||
rlblock="$(bsfn release_listing)"
|
||||
run_rl() { # listing-content
|
||||
printf '%s\n' "$1" > "$adir/rl.txt"
|
||||
A="$adir" bash -c '
|
||||
warn() { echo "WARN: $*"; }
|
||||
die() { echo "DIE: $*"; exit 1; }
|
||||
felis_asset_arch() { echo amd64; }
|
||||
artifact_fetch() { echo "FETCH $1"; ARTIFACT_FILE="$A/rl.txt"; }
|
||||
'"$unblock"'
|
||||
'"$llblock"'
|
||||
'"$rlblock"'
|
||||
ARTIFACT_MODE=release RELEASE_LISTING="" RELEASE_LISTING_STATE=""
|
||||
release_listing && echo FIRST-OK
|
||||
release_listing && echo SECOND-OK
|
||||
true'
|
||||
}
|
||||
out="$(run_rl "$good")"
|
||||
[ "$(printf '%s\n' "$out" | grep -c FETCH)" = 1 ] && expect "a good listing serves every lookup" "FIRST-OK
|
||||
SECOND-OK" "$out" \
|
||||
|| { echo "FAIL: the listing was fetched more than once: $out"; fails=$((fails + 1)); }
|
||||
out="$(run_rl "junk")"
|
||||
[ "$(printf '%s\n' "$out" | grep -c 'WARN:')" = 1 ] && [ "$(printf '%s\n' "$out" | grep -c FETCH)" = 1 ] \
|
||||
&& expect "a malformed listing is reported once and trusted nowhere" "is malformed; building its images on this host instead" "$out" \
|
||||
|| { echo "FAIL: a malformed listing was reported or fetched more than once: $out"; fails=$((fails + 1)); }
|
||||
case "$out" in *-OK*) echo "FAIL: a malformed listing was used"; fails=$((fails + 1)) ;; esac
|
||||
|
||||
# --- pushing a release image into the platform registry ----------------------------------------
|
||||
prblock="$(bsfn push_release_image)"
|
||||
run_push_release() { # role registry-digest-for-tag registry-digest-for-version-tag [MC_VERSION]
|
||||
REG_TAG="$2" REG_VER="$3" MCV="${4-26.2}" LINE="$1 felis-image-game-linux-amd64.tar registry.felis.svc:5000/felis/$1:demo registry.felis.svc:5000/felis/$1:demo $gdig $cdig" bash -c '
|
||||
die() { echo "DIE: $*"; exit 1; }
|
||||
warn() { echo "WARN: $*"; }
|
||||
log() { :; }
|
||||
ok() { echo "OK: $*"; }
|
||||
registry_manifest_digest() { case "$1" in *:demo) echo "$REG_TAG" ;; *) echo "$REG_VER" ;; esac; }
|
||||
artifact_fetch() { ARTIFACT_FILE="/cache/$1"; }
|
||||
felis() { echo "PUSH-IMAGE user=$FELIS_REGISTRY_USERNAME pass=$FELIS_REGISTRY_PASSWORD $*" >&2; echo "$gdig"; }
|
||||
HOST_BIN=felis REGISTRY_URL=registry.felis.svc:5000 REGISTRY_PUSH_HOST=127.0.0.1:5000
|
||||
REGISTRY_PLATFORM_TOKEN=tok MC_VERSION="$MCV" RELEASE_IMAGES="$LINE"
|
||||
'"$prblock"'
|
||||
push_release_image "${LINE%% *}"' 2>&1
|
||||
}
|
||||
out="$(run_push_release limbo "" "")"
|
||||
expect "a release image is pushed from its bundle by name, as the platform principal" \
|
||||
"PUSH-IMAGE user=platform pass=tok push-image --tar /cache/felis-image-game-linux-amd64.tar --image registry.felis.svc:5000/felis/limbo:demo --ref 127.0.0.1:5000/felis/limbo:demo" "$out"
|
||||
expect "and under its Minecraft version and config digest" "--ref 127.0.0.1:5000/felis/limbo:26.2-0123456789ab" "$out"
|
||||
out="$(run_push_release limbo "$gdig" "")"
|
||||
case "$out" in *"--ref 127.0.0.1:5000/felis/limbo:demo"*) echo "FAIL: a tag already naming the image was pushed again"; fails=$((fails + 1)) ;; *) echo "PASS a tag already naming the image is left as it is" ;; esac
|
||||
expect "while a missing version tag is still pushed" "--ref 127.0.0.1:5000/felis/limbo:26.2-0123456789ab" "$out"
|
||||
out="$(run_push_release limbo "sha256:$(printf other | sha256sum | cut -d' ' -f1)" "$gdig")"
|
||||
expect "a tag naming another build is pushed over" "--ref 127.0.0.1:5000/felis/limbo:demo" "$out"
|
||||
out="$(run_push_release felis "" "")"
|
||||
case "$out" in *"26.2-"*) echo "FAIL: the control-plane image got a Minecraft version tag"; fails=$((fails + 1)) ;; *) echo "PASS only game images get a version tag" ;; esac
|
||||
out="$(run_push_release limbo "" "" "")"
|
||||
case "$out" in *"-0123456789ab"*) echo "FAIL: a version tag was made without a Minecraft version"; fails=$((fails + 1)) ;; *) echo "PASS no Minecraft version, no version tag" ;; esac
|
||||
|
||||
rmblock="$(bsfn registry_manifest_digest)"
|
||||
curlargs="$(mktemp)"
|
||||
out="$(CA="$curlargs" bash -c 'curl() { printf "%s\n" "$*" > "$CA"; printf "HTTP/1.1 200 OK\r\nContent-Type: x\r\nDocker-Content-Digest: sha256:abc\r\n\r\n"; }
|
||||
'"$rmblock"'
|
||||
registry_manifest_digest 127.0.0.1:5000/felis/limbo:26.2-0123')"
|
||||
expect "the registry's digest for a tag is read off a HEAD" "-fsSI --max-time 30" "$(cat "$curlargs")"
|
||||
expect "from the tag's manifest URL" "http://127.0.0.1:5000/v2/felis/limbo/manifests/26.2-0123" "$(cat "$curlargs")"
|
||||
expect "asking for every manifest kind a registry may hold" "application/vnd.docker.distribution.manifest.list.v2+json" "$(cat "$curlargs")"
|
||||
rm -f "$curlargs"
|
||||
[ "$out" = "sha256:abc" ] && echo "PASS the digest comes back without its carriage return" \
|
||||
|| { echo "FAIL: registry_manifest_digest printed: $out"; fails=$((fails + 1)); }
|
||||
[ -z "$(bash -c 'curl() { return 22; }; '"$rmblock"'; registry_manifest_digest 127.0.0.1:5000/felis/x:demo')" ] \
|
||||
&& echo "PASS a tag the registry lacks has no digest" || { echo "FAIL: a missing tag printed a digest"; fails=$((fails + 1)); }
|
||||
|
||||
# --- the binary from FELIS_ARTIFACT_DIR -----------------------------------------------------
|
||||
iabblock="$(bsfn load_artifact_sums; bsfn artifact_sum; bsfn install_artifact_binary)"
|
||||
mkdir -p "$adir/abin/dir" "$adir/abin/bin"
|
||||
run_artifact_binary() { # binary-script
|
||||
printf '%s\n' "$1" > "$adir/abin/dir/felis-linux-amd64"
|
||||
printf '%s felis-linux-amd64\n' "$(sha "$adir/abin/dir/felis-linux-amd64")" > "$adir/abin/dir/SHA256SUMS"
|
||||
[ -z "${TAMPER:-}" ] || printf 'x\n' >> "$adir/abin/dir/felis-linux-amd64"
|
||||
A="$adir/abin" bash -c '
|
||||
set -Eeuo pipefail
|
||||
die() { echo "DIE: $*"; exit 1; }
|
||||
ok() { echo "OK: $*"; }
|
||||
remember_temp() { :; }
|
||||
keep_previous_host_binary() { echo KEEP; }
|
||||
felis_asset_arch() { echo amd64; }
|
||||
FELIS_ARTIFACT_DIR="$A/dir" HOST_BIN="$A/bin/felis" ARTIFACT_MODE="" ARTIFACT_SUMS="" HAVE_PREBUILT_BINARY="" FELIS_VERSION=""
|
||||
'"$iabblock"'
|
||||
install_artifact_binary
|
||||
echo "VERSION[$FELIS_VERSION] PREBUILT[$HAVE_PREBUILT_BINARY] MODE[$ARTIFACT_MODE]"' 2>&1
|
||||
}
|
||||
out="$(run_artifact_binary '#!/bin/sh
|
||||
echo "felis v9.9.9"')"
|
||||
expect "the directory's binary is installed and names the version" "VERSION[v9.9.9] PREBUILT[1] MODE[dir]" "$out"
|
||||
[ -x "$adir/abin/bin/felis" ] && echo "PASS the binary lands at HOST_BIN" || { echo "FAIL: no binary at HOST_BIN"; fails=$((fails + 1)); }
|
||||
out="$(run_artifact_binary '#!/bin/sh
|
||||
echo "felis v9.9.9"')"
|
||||
case "$out" in *KEEP*) echo "FAIL: the same binary was replaced (and its previous copy overwritten)"; fails=$((fails + 1)) ;; *) echo "PASS the same binary is left in place" ;; esac
|
||||
out="$(TAMPER=1 run_artifact_binary '#!/bin/sh
|
||||
echo "felis v9.9.9"')"
|
||||
expect "a binary that does not match SHA256SUMS stops the install" "DIE: FELIS_ARTIFACT_DIR: felis-linux-amd64 does not match SHA256SUMS" "$out"
|
||||
out="$(run_artifact_binary '#!/bin/sh
|
||||
exit 1')"
|
||||
expect "a binary that cannot say its version stops the install" "DIE: FELIS_ARTIFACT_DIR: felis-linux-amd64 reports 'nothing' as its version" "$out"
|
||||
rm -f "$adir/abin/dir/SHA256SUMS"
|
||||
out="$(A="$adir/abin" bash -c 'die() { echo "DIE: $*"; exit 1; }; remember_temp() { :; }; felis_asset_arch() { echo amd64; }
|
||||
FELIS_ARTIFACT_DIR="$A/dir" ARTIFACT_MODE="" ARTIFACT_SUMS=""
|
||||
'"$iabblock"'
|
||||
install_artifact_binary' 2>&1)"
|
||||
expect "a directory without SHA256SUMS stops the install" "DIE: FELIS_ARTIFACT_DIR: $adir/abin/dir holds no SHA256SUMS" "$out"
|
||||
|
||||
# --- which release the images come from -------------------------------------------------------
|
||||
srblock="$(bsfn select_release_artifacts)"
|
||||
run_select() { # binary-version sums-available(0|1) [ARTIFACT_MODE] [HAVE_PREBUILT_BINARY]
|
||||
printf '#!/bin/sh\necho "felis %s"\n' "$1" > "$adir/selbin"; chmod +x "$adir/selbin"
|
||||
SUMS_OK="$2" MODE="${3:-}" PRE="${4-1}" B="$adir/selbin" bash -c '
|
||||
ok() { echo "OK: $*"; }
|
||||
warn() { echo "WARN: $*"; }
|
||||
load_artifact_sums() { echo "SUMS $ARTIFACT_TAG"; [ "$SUMS_OK" = 1 ]; }
|
||||
ARTIFACT_MODE="$MODE" ARTIFACT_TAG="" HAVE_PREBUILT_BINARY="$PRE" HOST_BIN="$B"
|
||||
'"$srblock"'
|
||||
select_release_artifacts
|
||||
echo "MODE[$ARTIFACT_MODE] TAG[$ARTIFACT_TAG]"'
|
||||
}
|
||||
expect "a release binary takes its images from that release" "MODE[release] TAG[v9.9.9]" "$(run_select v9.9.9 1)"
|
||||
expect "a prerelease tag too" "MODE[release] TAG[v9.9.9-rc.1]" "$(run_select v9.9.9-rc.1 1)"
|
||||
out="$(run_select v9.9.9 0)"
|
||||
expect "a release without SHA256SUMS builds its images here" "MODE[] TAG[]" "$out"
|
||||
expect "and says what that costs" "this installs Docker and needs about 8 GiB more" "$out"
|
||||
out="$(run_select v0.0.0+gabc1234 1)"
|
||||
expect "a dev build names no release" "MODE[] TAG[]" "$out"
|
||||
case "$out" in *SUMS*) echo "FAIL: a dev build looked up a release"; fails=$((fails + 1)) ;; *) echo "PASS a dev build looks up no release" ;; esac
|
||||
case "$(run_select dev 1)" in *SUMS*) echo "FAIL: an unstamped build looked up a release"; fails=$((fails + 1)) ;; *) echo "PASS an unstamped build looks up no release" ;; esac
|
||||
expect "a source build takes nothing from a release" "MODE[] TAG[]" "$(run_select v9.9.9 1 "" "")"
|
||||
expect "FELIS_ARTIFACT_DIR stays the source" "MODE[dir] TAG[]" "$(run_select v9.9.9 1 dir)"
|
||||
|
||||
# --- the game stack and the Velocity plugin --------------------------------------------------
|
||||
gsblock="$(bsfn build_game_stack; bsfn role_prebuilt; bsfn stop_docker)"
|
||||
run_game_stack() { # PREBUILT_ROLES-after-import FELIS_GAME_STACK [ARTIFACT_MODE]
|
||||
AFTER="$1" STACK="$2" MODE="${3-release}" bash -c '
|
||||
ok() { :; }
|
||||
game_stack_source() { :; }
|
||||
resolve_game_jars() { :; }
|
||||
import_release_images() { echo "IMPORT $*"; PREBUILT_ROLES="$AFTER"; }
|
||||
ensure_docker() { echo ENSURE; DOCKER_INSTALLED=1; }
|
||||
build_game_image() { echo "BUILD $1"; }
|
||||
install_velocity_plugin() { echo PLUGIN; }
|
||||
systemctl() { echo "SYSTEMCTL $*"; }
|
||||
PREBUILT_ROLES=" " DOCKER_INSTALLED="" FELIS_GAME_STACK="$STACK" ARTIFACT_MODE="$MODE"
|
||||
'"$gsblock"'
|
||||
build_game_stack'
|
||||
}
|
||||
out="$(run_game_stack " limbo lobby paper " pinned)"
|
||||
expect "the pinned stack comes from the release" "IMPORT limbo lobby paper" "$out"
|
||||
case "$out" in *BUILD*|*ENSURE*|*SYSTEMCTL*) echo "FAIL: a release game stack built or touched docker: $out"; fails=$((fails + 1)) ;; *) echo "PASS a release game stack needs no docker" ;; esac
|
||||
out="$(run_game_stack " limbo " pinned)"
|
||||
expect "only what the release did not supply is built" "ENSURE
|
||||
BUILD lobby
|
||||
BUILD paper
|
||||
PLUGIN
|
||||
SYSTEMCTL stop docker docker.socket" "$out"
|
||||
out="$(run_game_stack " limbo lobby paper " latest)"
|
||||
case "$out" in *IMPORT*) echo "FAIL: FELIS_GAME_STACK=latest took the release's pinned images"; fails=$((fails + 1)) ;; *) echo "PASS FELIS_GAME_STACK=latest takes nothing from the release" ;; esac
|
||||
expect "and builds all three" "BUILD limbo
|
||||
BUILD lobby
|
||||
BUILD paper" "$out"
|
||||
|
||||
vpblock="$(bsfn install_velocity_plugin; bsfn artifact_unusable)"
|
||||
run_plugin() { # ARTIFACT_MODE fetch-ok(0|1)
|
||||
MODE="$1" FOK="$2" bash -c '
|
||||
die() { echo "DIE: $*"; exit 1; }
|
||||
warn() { echo "WARN: $*"; }
|
||||
ok() { echo "OK: $*"; }
|
||||
artifact_fetch() { ARTIFACT_FILE=/cache/felis-velocity.jar; [ "$FOK" = 1 ]; }
|
||||
prepare_velocity_layout() { echo LAYOUT; }
|
||||
install_if_changed() { echo "INSTALL $*"; }
|
||||
ensure_docker() { echo ENSURE; }
|
||||
build_velocity_plugin() { echo BUILD; }
|
||||
ARTIFACT_MODE="$MODE" VELOCITY_DIR=/opt/felis/velocity
|
||||
'"$vpblock"'
|
||||
install_velocity_plugin'
|
||||
}
|
||||
out="$(run_plugin release 1)"
|
||||
expect "the release's plugin is put in place" "LAYOUT
|
||||
INSTALL /cache/felis-velocity.jar /opt/felis/velocity/plugins/felis-velocity.jar 0644 root root" "$out"
|
||||
case "$out" in *BUILD*|*ENSURE*) echo "FAIL: a release plugin was built too"; fails=$((fails + 1)) ;; *) echo "PASS a release plugin is not built" ;; esac
|
||||
out="$(run_plugin release 0)"
|
||||
expect "an unusable release plugin is built here" "WARN: the release's felis-velocity.jar cannot be used; building it on this host instead
|
||||
ENSURE
|
||||
BUILD" "$out"
|
||||
expect "from FELIS_ARTIFACT_DIR it stops the install" "DIE: FELIS_ARTIFACT_DIR: the release's felis-velocity.jar cannot be used" "$(run_plugin dir 0)"
|
||||
expect "a source build builds the plugin" "ENSURE
|
||||
BUILD" "$(run_plugin "" 0)"
|
||||
|
||||
# --- k3s's own images from its GitHub release ----------------------------------------------------
|
||||
kablock="$(bsfn stage_k3s_airgap_images)"
|
||||
mkdir -p "$adir/k3simg" "$adir/k3srel"
|
||||
printf 'k3s images\n' > "$adir/k3srel/k3s-airgap-images-amd64.tar.zst"
|
||||
run_airgap() { # sums-file-content
|
||||
printf '%s\n' "$1" > "$adir/k3srel/sha256sum-amd64.txt"
|
||||
A="$adir" bash -c '
|
||||
set -Eeuo pipefail
|
||||
warn() { echo "WARN: $*"; }
|
||||
ok() { echo "OK: $*"; }
|
||||
log() { :; }
|
||||
remember_temp() { :; }
|
||||
felis_asset_arch() { echo amd64; }
|
||||
curl() {
|
||||
local out="" url
|
||||
while [ "$#" -gt 0 ]; do case "$1" in -o) out="$2"; shift 2 ;; *) url="$1"; shift ;; esac; done
|
||||
echo "CURL ${url##*/}" >&2
|
||||
[ -f "$A/k3srel/${url##*/}" ] || return 22
|
||||
if [ -n "$out" ]; then cp "$A/k3srel/${url##*/}" "$out"; else cat "$A/k3srel/${url##*/}"; fi
|
||||
}
|
||||
FELIS_K3S_VERSION=v1.36.4+k3s1 K3S_IMAGES_DIR="$A/k3simg"
|
||||
'"$kablock"'
|
||||
stage_k3s_airgap_images
|
||||
echo STAGED-OK' 2>&1
|
||||
}
|
||||
ksum="$(sha "$adir/k3srel/k3s-airgap-images-amd64.tar.zst")"
|
||||
out="$(run_airgap "$ksum k3s-airgap-images-amd64.tar.zst")"
|
||||
expect "k3s's images are downloaded where k3s imports them" "OK: staged k3s v1.36.4+k3s1's images" "$out"
|
||||
cmp -s "$adir/k3simg/k3s-airgap-images-amd64.tar.zst" "$adir/k3srel/k3s-airgap-images-amd64.tar.zst" \
|
||||
&& echo "PASS the staged tarball is the release's" || { echo "FAIL: the tarball was not staged"; fails=$((fails + 1)); }
|
||||
[ "$(ls -A "$adir/k3simg")" = k3s-airgap-images-amd64.tar.zst ] && echo "PASS no temp file is left where k3s reads" \
|
||||
|| { echo "FAIL: a temp file was left in the images directory: $(ls -A "$adir/k3simg")"; fails=$((fails + 1)); }
|
||||
out="$(run_airgap "$ksum k3s-airgap-images-amd64.tar.zst")"
|
||||
case "$out" in *"CURL k3s-airgap-images-amd64.tar.zst"*) echo "FAIL: staged images were downloaded again"; fails=$((fails + 1)) ;; *) echo "PASS staged images are not downloaded again" ;; esac
|
||||
rm -f "$adir/k3simg/k3s-airgap-images-amd64.tar.zst"
|
||||
out="$(run_airgap "$(printf '%064d' 0) k3s-airgap-images-amd64.tar.zst")"
|
||||
expect "a tarball that does not match k3s's sums is refused" "WARN: k3s-airgap-images-amd64.tar.zst hashes to $ksum" "$out"
|
||||
expect "and the install goes on without it" "STAGED-OK" "$out"
|
||||
[ -z "$(ls -A "$adir/k3simg")" ] && echo "PASS a refused tarball leaves nothing for k3s to import" \
|
||||
|| { echo "FAIL: a refused tarball was left: $(ls -A "$adir/k3simg")"; fails=$((fails + 1)); }
|
||||
out="$(run_airgap "$ksum k3s-airgap-images-arm64.tar.zst")"
|
||||
expect "a sums file that does not list the tarball stages nothing" "WARN: k3s v1.36.4+k3s1 lists no sha256 for k3s-airgap-images-amd64.tar.zst" "$out"
|
||||
case "$out" in *"CURL k3s-airgap-images-amd64.tar.zst"*) echo "FAIL: an unlisted tarball was downloaded"; fails=$((fails + 1)) ;; *) echo "PASS an unlisted tarball is not downloaded" ;; esac
|
||||
|
||||
# --- one release lookup per tag ------------------------------------------------------------------
|
||||
rjblock="$(bsfn load_release_json; bsfn github_asset_id)"
|
||||
out="$(bash -c '
|
||||
github_api() { echo "API $1" >&2; printf "{\"assets\":[{\"url\":\"https://api.github.com/repos/o/r/releases/assets/11\",\"name\":\"SHA256SUMS\"},{\"url\":\"https://api.github.com/repos/o/r/releases/assets/12\",\"name\":\"felis-velocity.jar\"}]}"; }
|
||||
repo_slug() { echo o/r; }
|
||||
RELEASE_JSON_TAG="" RELEASE_JSON=""
|
||||
'"$rjblock"'
|
||||
load_release_json v9.9.9
|
||||
github_asset_id v9.9.9 SHA256SUMS; github_asset_id v9.9.9 felis-velocity.jar' 2>&1)"
|
||||
[ "$(printf '%s\n' "$out" | grep -c '^API')" = 1 ] && echo "PASS one release is looked up once" \
|
||||
|| { echo "FAIL: the release was looked up more than once: $out"; fails=$((fails + 1)); }
|
||||
expect "and every asset is found in it" "11
|
||||
12" "$out"
|
||||
|
||||
# --- FELIS_ARTIFACT_DIR is checked before anything happens ------------------------------------
|
||||
vsblock="$(awk '/^ if \[ -n "\$FELIS_ARTIFACT_DIR" \]; then$/ { f = 1 } f { print } f && /^ fi$/ { exit }' "$BS")"
|
||||
case "$vsblock" in *"FELIS_SKIP_FETCH both"*) ;; *) echo "FAIL: the FELIS_ARTIFACT_DIR checks in validate_settings moved"; exit 1 ;; esac
|
||||
run_vs() { # FELIS_ARTIFACT_DIR [FELIS_REF_PINNED]
|
||||
FELIS_ARTIFACT_DIR="$1" FELIS_REF_PINNED="${2:-}" bash -c 'die() { echo "DIE: $*"; exit 1; }
|
||||
'"$vsblock"'
|
||||
echo VALID'
|
||||
}
|
||||
expect "a relative FELIS_ARTIFACT_DIR is refused" "DIE: FELIS_ARTIFACT_DIR must be an absolute path" "$(run_vs release-assets)"
|
||||
expect "a FELIS_ARTIFACT_DIR without SHA256SUMS is refused up front" "SHA256SUMS does not exist" "$(run_vs "$adir/nowhere")"
|
||||
printf 'x y\n' > "$adir/SHA256SUMS"
|
||||
expect "a FELIS_ARTIFACT_DIR with SHA256SUMS is accepted" "VALID" "$(run_vs "$adir")"
|
||||
expect "FELIS_ARTIFACT_DIR and FELIS_REF together are refused" "DIE: FELIS_ARTIFACT_DIR and FELIS_REF both name what to install" "$(run_vs "$adir" 1)"
|
||||
rm -rf "$adir"
|
||||
|
||||
# ---------------------------------------------------------------------------------------
|
||||
if [ "$fails" -eq 0 ]; then
|
||||
echo "ALL PASS"
|
||||
|
||||
Reference in new issue
Block a user