From ffbd905ec4c7f483cc1e9ccdfa6ae12f236b5e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chip=20Wolf=20=E2=80=AE?= Date: Wed, 13 May 2026 00:56:30 +0100 Subject: [PATCH] feat: load env vars from file or archive at startup (#4053) --- docs/mods-and-plugins/index.md | 48 +++++++ scripts/start-configuration | 25 ++++ scripts/start-utils | 119 +++++++++++++++++- .../load-env-from-archive/docker-compose.yml | 18 +++ .../load-env-from-archive/fake.jar | 0 .../load-env-from-archive/load-env.zip | Bin 0 -> 221 bytes .../load-env-from-archive/verify.sh | 1 + .../load-env-from-file/docker-compose.yml | 18 +++ .../load-env-from-file/fake.jar | 0 .../load-env-from-file/load-env.env | 2 + .../load-env-from-file/verify.sh | 1 + .../docker-compose.yml | 19 +++ .../load-env-from-generic-pack/fake.jar | 0 .../load-env-from-generic-pack/pack.zip | Bin 0 -> 486 bytes .../load-env-from-generic-pack/verify.sh | 2 + 15 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 tests/setuponlytests/load-env-from-archive/docker-compose.yml create mode 100644 tests/setuponlytests/load-env-from-archive/fake.jar create mode 100644 tests/setuponlytests/load-env-from-archive/load-env.zip create mode 100644 tests/setuponlytests/load-env-from-archive/verify.sh create mode 100644 tests/setuponlytests/load-env-from-file/docker-compose.yml create mode 100644 tests/setuponlytests/load-env-from-file/fake.jar create mode 100644 tests/setuponlytests/load-env-from-file/load-env.env create mode 100644 tests/setuponlytests/load-env-from-file/verify.sh create mode 100644 tests/setuponlytests/load-env-from-generic-pack/docker-compose.yml create mode 100644 tests/setuponlytests/load-env-from-generic-pack/fake.jar create mode 100644 tests/setuponlytests/load-env-from-generic-pack/pack.zip create mode 100644 tests/setuponlytests/load-env-from-generic-pack/verify.sh diff --git a/docs/mods-and-plugins/index.md b/docs/mods-and-plugins/index.md index 8d91e3f4..f59219f0 100644 --- a/docs/mods-and-plugins/index.md +++ b/docs/mods-and-plugins/index.md @@ -156,6 +156,54 @@ Disabling mods within docker compose files: mod2.jar ``` +### Loading container configuration from a pack + +A pack can ship its own container configuration so that the server type, version, +and other variables travel with the pack rather than being declared by the user. +At startup, before `TYPE` is dispatched, the container can load environment +variables from a file on disk, a URL, an entry inside an archive, or from the +`.env` of each `GENERIC_PACK(S)` entry. + +- `LOAD_ENV_FROM_GENERIC_PACK`: when `true`, each entry in `GENERIC_PACKS` (after + `GENERIC_PACKS_PREFIX`/`SUFFIX` expansion) is probed for a top-level `.env` + and each one found is sourced in the same order the packs are applied (later + packs override earlier ones, matching the layering of the unpack itself). Packs + without a `.env` are skipped without error. URLs are downloaded into + `/data/packs/` and reused by the regular generic-pack unpack step, so they are + not fetched twice. +- `LOAD_ENV_FROM_FILE`: container path or URL of a shell-style env file (one + `KEY=VALUE` per line). Comments and blank lines are allowed. +- `LOAD_ENV_FROM_ARCHIVE`: container path or URL of a zip/tar archive containing + an env file. The entry is sourced into the environment. +- `LOAD_ENV_FROM_ARCHIVE_ENTRY`: relative path of the env file inside the archive. + Defaults to `.env`. + +These can be combined. Load order is: generic packs first, then +`LOAD_ENV_FROM_FILE`, then `LOAD_ENV_FROM_ARCHIVE` — later loads override +earlier ones, and all of them **override** values passed via `docker run -e` (or +compose `environment:`), so the pack's declared values win. + +```shell +docker run -d \ + -e EULA=TRUE \ + -e GENERIC_PACK=https://cdn.example.org/my-pack.zip \ + -e LOAD_ENV_FROM_GENERIC_PACK=true \ + itzg/minecraft-server +``` + +Where `my-pack.zip` contains a `.env` at its root such as: + +```env +TYPE=FABRIC +VERSION=1.21.1 +FABRIC_LOADER_VERSION=0.16.0 +``` + +!!! warning + The env file is sourced by `bash`, so any shell syntax it contains will be + evaluated. Only point these variables at sources you trust. `EULA` cannot be + set this way — it is checked before the env file is loaded. + ## Mods/plugins list You may also download or copy over individual mods/plugins using the `MODS` or `PLUGINS` environment variables. Both are a comma or newline delimited list of diff --git a/scripts/start-configuration b/scripts/start-configuration index 13838dfa..386b7616 100755 --- a/scripts/start-configuration +++ b/scripts/start-configuration @@ -152,6 +152,31 @@ fi cd /data || exit 1 +########################################## +# Optionally load environment variables from a file or archive entry, +# allowing packs/artifacts to declare TYPE, VERSION and other settings +# inside-out. Loaded values override anything passed in through docker -e. +# Generic packs are processed first so that LOAD_ENV_FROM_FILE and +# LOAD_ENV_FROM_ARCHIVE can override any values they set. + +if isTrue "${LOAD_ENV_FROM_GENERIC_PACK:-false}"; then + if ! loadEnvFromGenericPack; then + exit 1 + fi +fi + +if [[ ${LOAD_ENV_FROM_FILE:-} ]]; then + if ! loadEnvFromFile "${LOAD_ENV_FROM_FILE}"; then + exit 1 + fi +fi + +if [[ ${LOAD_ENV_FROM_ARCHIVE:-} ]]; then + if ! loadEnvFromArchive "${LOAD_ENV_FROM_ARCHIVE}" "${LOAD_ENV_FROM_ARCHIVE_ENTRY:-.env}"; then + exit 1 + fi +fi + export DECLARED_TYPE=${TYPE^^} export DECLARED_VERSION="$VERSION" diff --git a/scripts/start-utils b/scripts/start-utils index 34963936..62ba29e6 100755 --- a/scripts/start-utils +++ b/scripts/start-utils @@ -26,6 +26,117 @@ function applyResultsFile() { set +a } +function loadEnvFromFile() { + local source=${1?Missing required source argument} + local downloaded= + + if isURL "$source"; then + mkdir -p /data/.tmp + downloaded=$(mktemp -p /data/.tmp) + log "Downloading env file from $source" + if ! get -o "$downloaded" "$source"; then + logError "Failed to download env file from $source" + rm -f "$downloaded" + return 1 + fi + log "Loading env vars from $source" + applyResultsFile "$downloaded" + rm -f "$downloaded" + elif [ -f "$source" ]; then + log "Loading env vars from $source" + applyResultsFile "$source" + else + logError "Env file not found: $source" + return 1 + fi +} + +function loadEnvFromArchive() { + local source=${1?Missing required source argument} + local entry=${2:-.env} + local archive= + local downloaded= + local tmpdir + local rc=0 + + mkdir -p /data/.tmp + + if isURL "$source"; then + downloaded=$(mktemp -p /data/.tmp) + log "Downloading archive from $source" + if ! get -o "$downloaded" "$source"; then + logError "Failed to download archive from $source" + rm -f "$downloaded" + return 1 + fi + archive=$downloaded + elif [ -f "$source" ]; then + archive=$source + else + logError "Archive not found: $source" + return 1 + fi + + tmpdir=$(mktemp -d -p /data/.tmp) + if extract "$archive" "$tmpdir" "$entry" && [ -f "$tmpdir/$entry" ]; then + log "Loading env vars from '$entry' in $source" + applyResultsFile "$tmpdir/$entry" + else + logError "Failed to load env entry '$entry' from $source" + rc=1 + fi + + rm -rf "$tmpdir" + [[ -n "$downloaded" ]] && rm -f "$downloaded" + return $rc +} + +function loadEnvFromGenericPack() { + : "${GENERIC_PACKS:=${GENERIC_PACK:-}}" + : "${GENERIC_PACKS_PREFIX:=}" + : "${GENERIC_PACKS_SUFFIX:=}" + + if [[ -z "${GENERIC_PACKS}" ]]; then + logWarning "LOAD_ENV_FROM_GENERIC_PACK is set but GENERIC_PACK(S) is empty" + return 0 + fi + + mkdir -p /data/.tmp + IFS=',' read -ra packs <<< "${GENERIC_PACKS}" + local loaded=0 + local pack packEntry packFile tmpdir + for packEntry in "${packs[@]}"; do + pack="${GENERIC_PACKS_PREFIX}${packEntry}${GENERIC_PACKS_SUFFIX}" + if isURL "$pack"; then + mkdir -p /data/packs + if ! packFile=$(get -o /data/packs --output-filename --skip-up-to-date "$pack"); then + logError "Failed to download generic pack $pack" + return 1 + fi + else + packFile=$pack + fi + + if [[ ! -f "$packFile" ]]; then + logError "Generic pack not found: $packFile" + return 1 + fi + + tmpdir=$(mktemp -d -p /data/.tmp) + # Packs without a .env are valid — silently skip; the unpack step still applies them. + if extract "$packFile" "$tmpdir" .env 2>/dev/null && [ -f "$tmpdir/.env" ]; then + log "Loading env vars from .env in $pack" + applyResultsFile "$tmpdir/.env" + loaded=$((loaded + 1)) + fi + rm -rf "$tmpdir" + done + + if (( loaded == 0 )); then + logWarning "LOAD_ENV_FROM_GENERIC_PACK is set but no pack in GENERIC_PACK(S) contained a .env" + fi +} + function join_by() { local d=$1 shift @@ -445,17 +556,19 @@ function isType() { function extract() { src=${1?} destDir=${2?} + shift 2 + # remaining args are paths within the archive to extract; if none, extract everything type=$(file -b --mime-type "${src}") case "${type}" in application/zip) - unzip -o -q -d "${destDir}" "${src}" + unzip -o -q -d "${destDir}" "${src}" "$@" ;; application/x-tar | application/gzip | application/x-gzip | application/x-bzip2) - tar -C "${destDir}" -xf "${src}" + tar -C "${destDir}" -xf "${src}" "$@" ;; application/zstd | application/x-zstd) - tar -C "${destDir}" --use-compress-program=unzstd -xf "${src}" + tar -C "${destDir}" --use-compress-program=unzstd -xf "${src}" "$@" ;; *) logError "Unsupported archive type: $type" diff --git a/tests/setuponlytests/load-env-from-archive/docker-compose.yml b/tests/setuponlytests/load-env-from-archive/docker-compose.yml new file mode 100644 index 00000000..d54e3e35 --- /dev/null +++ b/tests/setuponlytests/load-env-from-archive/docker-compose.yml @@ -0,0 +1,18 @@ +services: + mc: + image: ${IMAGE_TO_TEST:-itzg/minecraft-server} + environment: + EULA: "true" + SETUP_ONLY: "true" + LOAD_ENV_FROM_ARCHIVE: /test/load-env.zip + MOTD: from-compose + LOG_TIMESTAMP: "true" + DEBUG: "true" + # the following are only used to speed up test execution + TYPE: CUSTOM + CUSTOM_SERVER: /servers/fake.jar + VERSION: 1.18.1 + volumes: + - ./data:/data + - ./load-env.zip:/test/load-env.zip + - ./fake.jar:/servers/fake.jar diff --git a/tests/setuponlytests/load-env-from-archive/fake.jar b/tests/setuponlytests/load-env-from-archive/fake.jar new file mode 100644 index 00000000..e69de29b diff --git a/tests/setuponlytests/load-env-from-archive/load-env.zip b/tests/setuponlytests/load-env-from-archive/load-env.zip new file mode 100644 index 0000000000000000000000000000000000000000..efec7c1494f3b692e0387251599b576c583d2813 GIT binary patch literal 221 zcmWIWW@h1H0D(J`R>cHYGqC#u*&xgU#CoZDWy%UZ`H3m1DGEuI3O@dhF7d8@VexK3 z{=V^!LCzkYVXg`(rA3)}=?cXqiA5#4$@zI{ndzlPi6xo&d0f8!AuhIQMfth9iABj7 znPsV50p5&E_6)eJQ~_EF0t#Rf;s7ovkC8!w!I4#M@xljp7@^W|D!`kS4a8vt!Z09R H2$lf=Brz2y{H~g z0qO<;1uzNm4i}Wi$RNRR%;|~Qp$B&uq0(>)Vk$0Y!%PLaEdhw3o