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 00000000..efec7c14 Binary files /dev/null and b/tests/setuponlytests/load-env-from-archive/load-env.zip differ diff --git a/tests/setuponlytests/load-env-from-archive/verify.sh b/tests/setuponlytests/load-env-from-archive/verify.sh new file mode 100644 index 00000000..03f06943 --- /dev/null +++ b/tests/setuponlytests/load-env-from-archive/verify.sh @@ -0,0 +1 @@ +mc-image-helper assert propertyEquals --file=server.properties --property=motd --expect=from-archive diff --git a/tests/setuponlytests/load-env-from-file/docker-compose.yml b/tests/setuponlytests/load-env-from-file/docker-compose.yml new file mode 100644 index 00000000..336173e0 --- /dev/null +++ b/tests/setuponlytests/load-env-from-file/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_FILE: /test/load-env.env + 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.env:/test/load-env.env + - ./fake.jar:/servers/fake.jar diff --git a/tests/setuponlytests/load-env-from-file/fake.jar b/tests/setuponlytests/load-env-from-file/fake.jar new file mode 100644 index 00000000..e69de29b diff --git a/tests/setuponlytests/load-env-from-file/load-env.env b/tests/setuponlytests/load-env-from-file/load-env.env new file mode 100644 index 00000000..419082c7 --- /dev/null +++ b/tests/setuponlytests/load-env-from-file/load-env.env @@ -0,0 +1,2 @@ +# Loaded by LOAD_ENV_FROM_FILE during start-configuration +MOTD=from-env-file diff --git a/tests/setuponlytests/load-env-from-file/verify.sh b/tests/setuponlytests/load-env-from-file/verify.sh new file mode 100644 index 00000000..225c01a0 --- /dev/null +++ b/tests/setuponlytests/load-env-from-file/verify.sh @@ -0,0 +1 @@ +mc-image-helper assert propertyEquals --file=server.properties --property=motd --expect=from-env-file diff --git a/tests/setuponlytests/load-env-from-generic-pack/docker-compose.yml b/tests/setuponlytests/load-env-from-generic-pack/docker-compose.yml new file mode 100644 index 00000000..d68d0f02 --- /dev/null +++ b/tests/setuponlytests/load-env-from-generic-pack/docker-compose.yml @@ -0,0 +1,19 @@ +services: + mc: + image: ${IMAGE_TO_TEST:-itzg/minecraft-server} + environment: + EULA: "true" + SETUP_ONLY: "true" + GENERIC_PACK: /packs/pack.zip + LOAD_ENV_FROM_GENERIC_PACK: "true" + 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 + - ./pack.zip:/packs/pack.zip + - ./fake.jar:/servers/fake.jar diff --git a/tests/setuponlytests/load-env-from-generic-pack/fake.jar b/tests/setuponlytests/load-env-from-generic-pack/fake.jar new file mode 100644 index 00000000..e69de29b diff --git a/tests/setuponlytests/load-env-from-generic-pack/pack.zip b/tests/setuponlytests/load-env-from-generic-pack/pack.zip new file mode 100644 index 00000000..6345a6d1 Binary files /dev/null and b/tests/setuponlytests/load-env-from-generic-pack/pack.zip differ diff --git a/tests/setuponlytests/load-env-from-generic-pack/verify.sh b/tests/setuponlytests/load-env-from-generic-pack/verify.sh new file mode 100644 index 00000000..87d775d3 --- /dev/null +++ b/tests/setuponlytests/load-env-from-generic-pack/verify.sh @@ -0,0 +1,2 @@ +mc-image-helper assert propertyEquals --file=server.properties --property=motd --expect=from-generic-pack +mc-image-helper assert fileExists config/dummy.yml