Something went wrong. Try again.
Backend environment for match hosting for lance.blue
Something went wrong. Try again.
Shell
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185#!/usr/bin/env bash# Shared helpers for the container's init, watch and exit scripts.# Sourced, never executed.## . "$ARENA_HOME/container/lib/common.sh"
# Layout inside the image. Overridable so the same scripts run from a checkout# during local development.ARENA_HOME="${ARENA_HOME:-/opt/arena}"ARENA_RUN="${ARENA_RUN:-/run/arena}" # per-match scratch: manifest, config, stateARENA_SPOOL="${ARENA_SPOOL:-$ARENA_RUN/spool}" # turn reports awaiting uploadARENA_STATE="${ARENA_STATE:-$ARENA_RUN/state}" # host.status, result.json, game-over# The page Suramadu serves, staged here by init/40-render-config.sh rather than# served from the image: watch/results.sh writes the result into it when the# game is decided, and the image tree is not writable by the user this runs as.ARENA_WEB="${ARENA_WEB:-$ARENA_RUN/web}"MANIFEST="${MANIFEST:-$ARENA_RUN/manifest.json}"MM_HOME="${MM_HOME:-$ARENA_HOME/megamek}"
# shellcheck disable=SC2034 # used by the scripts that source this filePORT_WS="${ARENA_PORT_WS:-8080}"# shellcheck disable=SC2034PORT_MM="${ARENA_PORT_MM:-8850}"
log() { printf '[%s] %s\n' "${ARENA_LOG_TAG:-arena}" "$*" >&2; }warn() { printf '[%s] WARN: %s\n' "${ARENA_LOG_TAG:-arena}" "$*" >&2; }die() { printf '[%s] ERROR: %s\n' "${ARENA_LOG_TAG:-arena}" "$*" >&2; exit 1; }
# Anything derived from the manifest may embed a presigned URL, which is a bearer# credential. Log through this, or log field names and digests instead of values.redact() { sed -E \ -e 's#([?&](X-Amz-Signature|Signature|sig|token)=)[^ "&]*#\1REDACTED#Ig' \ -e 's|(Authorization: *).*|\1REDACTED|I'}
# curl with retries and no credential leakage into the log.# try_fetch <url> <dest> -> non-zero on failure, caller decidestry_fetch() { local url="$1" dest="$2" curl -fsSL --retry 4 --retry-delay 2 --max-time "${ARENA_FETCH_TIMEOUT:-120}" \ -o "$dest" "$url"}
# fetch <url> <dest> -> fatal on failure## Note this exits the *calling script*, not just the function, so it cannot be# used in an `if ! fetch ...` test. Use try_fetch where failure is recoverable -# camo, for instance, is cosmetic and must not cost anyone a match.fetch() { try_fetch "$1" "$2" || die "download failed for $(printf '%s' "$1" | redact)"}
# jq against the manifest. mq <filter> [default]mq() { local out out="$(jq -r "$1 // empty" "$MANIFEST" 2>/dev/null || true)" [ -n "$out" ] && { printf '%s' "$out"; return 0; } [ $# -ge 2 ] && printf '%s' "$2" return 0}
# Best-effort status callback. A failed callback never fails the match -# headquarters treats task exit as authoritative.# emit <event> [detail]emit() { local event="$1" detail="${2:-}" url url="$(mq '.callback.url')" [ -n "$url" ] || return 0 local body body="$(jq -nc --arg e "$event" --arg d "$detail" --arg m "$(mq '.matchId')" \ '{matchId:$m, event:$e, detail:$d}')" curl -fsS --max-time 10 --retry 2 -X POST -H 'Content-Type: application/json' \ -d "$body" "$url" >/dev/null 2>&1 \ || warn "callback '$event' failed (ignored)"}
# Resolve the signing endpoint for one artifact under manifest .upload.<key>:# a fixed .signUrl, or a .signTemplate whose {name} is the file's basename.# signing_endpoint <key> <file> [name]## `name` overrides what the template's {name} is filled with, which is how an# artifact lands under a sub-prefix: the key gives `derived/{name}` and the# caller passes `turns/003-r02-FIRING.txt`. Defaults to the file's basename,# which is what every caller wanted before there were sub-prefixes.signing_endpoint() { local key="$1" file="$2" name="${3:-}" url template [ -n "$name" ] || name="$(basename "$file")" url="$(mq ".upload.\"$key\".signUrl")" template="$(mq ".upload.\"$key\".signTemplate")" if [ -z "$url" ] && [ -n "$template" ]; then url="${template//\{name\}/$name}" fi printf '%s' "$url"}
# Whether this manifest offers an upload key at all.## headquarters names the layout, not arena: an image that knows `raw/` still# has to work against a control plane that has not deployed it yet, and the# manifest is where the two find out about each other. See `upload_class`.has_upload_key() { # <key> [ -n "$(mq ".upload.\"$1\".signUrl")" ] || [ -n "$(mq ".upload.\"$1\".signTemplate")" ]}
# Ask the control plane to sign a PUT for one artifact, and print the URL.## Kept separate from upload() so a minting failure is one testable thing.# curl's stderr is dropped rather than logged: its messages quote the URL, and# both the endpoint and the answer are bearer credentials. The caller says what# failed instead.# mint_upload_url <signing-endpoint>mint_upload_url() { local body body="$(curl -fsS --retry 2 --retry-delay 1 --max-time "${ARENA_MINT_TIMEOUT:-20}" \ -X POST -H 'Content-Length: 0' "$1" 2>/dev/null)" || return 1 printf '%s' "$body" | jq -re '.url // empty'}
# upload <local-file> <upload-key># upload-key indexes manifest .upload.<key>, which carries a signing endpoint# (.signUrl or .signTemplate). That is the only route: arena mints a presigned# PUT and sends the bytes to S3 itself.## There used to be a fallback that PUT the artifact to headquarters' own API and# let it re-send the bytes. It is gone. It read whole artifacts into memory on a# t4g.nano that also carries the API and Caddy, and a match's screenshots,# summary GIF, profiles and logs all arrive within a few seconds of each other# at the end. Keeping it as a fallback meant a broken signing setup degraded to# the slow route silently instead of saying so. Now a minting failure fails the# upload: the artifact stays in the container and the log says which key.## The signature is minted here, immediately before use, so its life never has to# cover the length of a match. That is not a detail: a presigned URL cannot# outlive the credentials that signed it, headquarters signs with IMDS# instance-profile credentials, and IMDS rotates those on its own schedule and# will not issue a fresh set on request. Signed at launch, every upload URL# would expire on a clock nobody can predict.upload() { local file="$1" key="$2" name="${3:-}" signer target signer="$(signing_endpoint "$key" "$file" "$name")" [ -n "$signer" ] || { log "no upload target for '$key'; keeping $file locally"; return 1; }
target="$(mint_upload_url "$signer")" && [ -n "$target" ] \ || { warn "could not mint an upload URL for '$key'; keeping $file locally"; return 1; }
# PUT, not the manifest's .method: the signature covers the method. curl -fsS --retry 3 --retry-delay 2 --max-time "${ARENA_UPLOAD_TIMEOUT:-120}" \ -X PUT --upload-file "$file" "$target" >/dev/null \ || { warn "upload of $(basename "$file") to '$key' failed"; return 1; } log "uploaded $(basename "$file") -> $key/${name:-$(basename "$file")}"}
# upload_class <file> <key> <name> [<legacy-key> [<legacy-name>]]## Send an artifact under the layout this manifest speaks.## The prefixes name what a file is - what the match was given, what MegaMek# wrote, what we made of it - and headquarters offers them as `launch`, `raw`,# `derived` and `socials`. A control plane that predates them offers only the# four keys named for the script that sent them, so each caller names the# legacy key and the name that layout expected, and gets today's behaviour# against yesterday's manifest.## No legacy key means an artifact that was never collected before: it is sent# when the manifest knows where to put it and skipped, quietly, when it does# not.upload_class() { local file="$1" key="$2" name="$3" legacy="${4:-}" legacy_name="${5:-}" if ! has_upload_key "$key" && [ -n "$legacy" ] && has_upload_key "$legacy"; then upload "$file" "$legacy" "${legacy_name:-$(basename "$file")}" return fi # Including when neither key is there: `upload` is what says which file was # offered and had nowhere to go, and a dev manifest with no targets at all # is a supported way to run this. upload "$file" "$key" "$name"}
require_cmd() { for c in "$@"; do command -v "$c" >/dev/null || die "'$c' not found in the image" done}