diff --git a/container/lib/common.sh b/container/lib/common.sh index e68e170..3686543 100644 --- a/container/lib/common.sh +++ b/container/lib/common.sh @@ -77,17 +77,32 @@ emit() { # Resolve the signing endpoint for one artifact under manifest .upload.: # a fixed .signUrl, or a .signTemplate whose {name} is the file's basename. -# signing_endpoint +# signing_endpoint [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" url template + 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\}/$(basename "$file")}" + 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() { # + [ -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. @@ -122,8 +137,8 @@ mint_upload_url() { # 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" signer target - signer="$(signing_endpoint "$key" "$file")" + 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" ] \ @@ -133,7 +148,33 @@ upload() { 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" + log "uploaded $(basename "$file") -> $key/${name:-$(basename "$file")}" +} + +# upload_class [ []] +# +# 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() { diff --git a/tests/shell/test-upload.sh b/tests/shell/test-upload.sh index d8a9dd9..467b56b 100644 --- a/tests/shell/test-upload.sh +++ b/tests/shell/test-upload.sh @@ -110,4 +110,41 @@ check "and names the key that had none" \ grep -q "no upload target for 'diagnostics'" <<<"$out" check "having sent nothing anywhere" test ! -s "$TMP/requests" +# --- the layout keys, and the ones they replaced ------------------------------ +# headquarters names the layout, so an image that knows `raw/` has to work +# against a control plane that has not deployed it yet, and vice versa. Each +# caller names the key it wants and the key that layout replaced. +run_class() { # + MANIFEST="$TMP/manifest.json" ARENA_LOG_TAG=test \ + bash -c 'set -uo pipefail; . "$0"; upload_class "$1" "$2" "$3" "$4" "$5"' \ + "$ROOT/container/lib/common.sh" "$TMP/megamek.log" "$1" "$2" "$3" "$4" 2>&1 +} + +: > "$TMP/requests" +manifest "{\"socials\":{\"signTemplate\":\"$BASE/sign/derived/socials/{name}\"},\ +\"screenshots\":{\"signTemplate\":\"$BASE/sign/shots/{name}\"}}" +out="$(run_class socials share-card.png screenshots share-card.png)" + +check "a manifest that knows the layout uses it" \ + grep -qx 'POST /sign/derived/socials/share-card.png' "$TMP/requests" +check "and does not also write the old name" bash -c \ + "! grep -qx 'POST /sign/shots/share-card.png' \"\$1/requests\"" _ "$TMP" + +: > "$TMP/requests" +manifest "{\"screenshots\":{\"signTemplate\":\"$BASE/sign/shots/{name}\"}}" +out="$(run_class socials share-card.png screenshots share-card.png)" + +check "a manifest without it falls back" \ + grep -qx 'POST /sign/shots/share-card.png' "$TMP/requests" + +# An artifact with no older home is skipped rather than misfiled: there is no +# right place for MegaMek's action log in a layout that predates `raw/`. +: > "$TMP/requests" +out="$(MANIFEST="$TMP/manifest.json" ARENA_LOG_TAG=test \ + bash -c 'set -uo pipefail; . "$0"; upload_class "$1" raw game_actions_0.tsv' \ + "$ROOT/container/lib/common.sh" "$TMP/megamek.log" 2>&1)" +check "an artifact with no older home is not misfiled" bash -c \ + "! grep -q 'POST /sign' \"\$1/requests\"" _ "$TMP" +check "and says which key had no target" grep -q "no upload target for 'raw'" <<<"$out" + exit "$fail"