diff --git a/.github/workflows/discord.yml b/.github/workflows/discord.yml index e4e6b42..495b6e4 100644 --- a/.github/workflows/discord.yml +++ b/.github/workflows/discord.yml @@ -2,7 +2,7 @@ name: discord on: workflow_run: - workflows: ["ContinuousIntegration", "PullRequest", "Build and Publish", "Build and publish multiarch" ] + workflows: ["ContinuousIntegration", "Build and Publish", "Build and publish multiarch" ] types: - completed diff --git a/Dockerfile b/Dockerfile index f363857..e7f9290 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive \ apt-get install -y \ imagemagick \ + file \ gosu \ sudo \ net-tools \ @@ -60,7 +61,7 @@ RUN easy-add --var os=${TARGETOS} --var arch=${TARGETARCH}${TARGETVARIANT} \ --var version=0.1.1 --var app=maven-metadata-release --file {{.app}} \ --from https://github.com/itzg/{{.app}}/releases/download/{{.version}}/{{.app}}_{{.version}}_{{.os}}_{{.arch}}.tar.gz -ARG MC_HELPER_VERSION=1.11.0 +ARG MC_HELPER_VERSION=1.16.0 ARG MC_HELPER_BASE_URL=https://github.com/itzg/mc-image-helper/releases/download/v${MC_HELPER_VERSION} RUN curl -fsSL ${MC_HELPER_BASE_URL}/mc-image-helper-${MC_HELPER_VERSION}.tgz \ | tar -C /usr/share -zxf - \ diff --git a/README.md b/README.md index 5a0922e..b29be7f 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ By default, the container will download the latest version of the "vanilla" [Min * [Running on RaspberryPi](#running-on-raspberrypi) * [Contributing](#contributing) - + @@ -481,10 +481,9 @@ An [Airplane](https://airplane.gg) server, which is "a stable, optimized, well s -e TYPE=AIRPLANE -> NOTE: The `VERSION` variable is used to select an Airplane branch to download from. The available options are "LATEST" "1.17" and "PURPUR" +> NOTE: The `VERSION` variable is used to select an Airplane type to download. The available options are "LATEST" and "PURPUR", both 1.17.1. Airplane does not support 1.18 -- use Paper/Pufferfish/Purpur. Extra variables: -- `AIRPLANE_BUILD=lastSuccessfulBuild` : set a specific Airplane build to use - `FORCE_REDOWNLOAD=false` : set to true to force the located server jar to be re-downloaded - `USE_FLARE_FLAGS=false` : set to true to add appropriate flags for the built-in [Flare](https://blog.airplane.gg/flare) profiler @@ -830,23 +829,19 @@ before unpacking new content from the MODPACK or MODS. ### Downloadable world -Instead of mounting the `/data` volume, you can instead specify the URL of a ZIP file containing an archived world. It will be searched for a file `level.dat` and the containing subdirectory moved to the directory named by `$LEVEL`. This means that most of the archived Minecraft worlds downloadable from the Internet will already be in the correct format. +Instead of mounting the `/data` volume, you can instead specify the URL of a ZIP or compressed TAR file containing an archived world. It will be searched for a file `level.dat` and the containing subdirectory moved to the directory named by `$LEVEL`. This means that most of the archived Minecraft worlds downloadable from the Internet will already be in the correct format. docker run -d -e WORLD=http://www.example.com/worlds/MySave.zip ... -**NOTE:** This URL must be accessible from inside the container. Therefore, -you should use an IP address or a globally resolvable FQDN, or else the -name of a linked container. +**NOTE:** This URL must be accessible from inside the container. Therefore, you should use an IP address or a globally resolvable FQDN, or else the name of a linked container. **NOTE:** If the archive contains more than one `level.dat`, then the one to select can be picked with `WORLD_INDEX`, which defaults to 1. ### Cloning world from a container path -The `WORLD` option can also be used to reference a directory or zip file that will be used as a source to clone or unzip the world directory. +The `WORLD` option can also be used to reference a directory, zip file, or compressed tar file that will be used as a source to clone or extract the world directory. -For example, the following would initially clone the world's content -from `/worlds/basic`. Also notice in the example that you can use a -read-only volume attachment to ensure the clone source remains pristine. +For example, the following would initially clone the world's content from `/worlds/basic`. Also notice in the example that you should use a read-only volume attachment to ensure the clone source remains pristine. ``` docker run ... -v $HOME/worlds:/worlds:ro -e WORLD=/worlds/basic diff --git a/scripts/start-deployAirplane b/scripts/start-deployAirplane index cb36aa8..2229a6b 100755 --- a/scripts/start-deployAirplane +++ b/scripts/start-deployAirplane @@ -6,35 +6,33 @@ isDebugging && set -x IFS=$'\n\t' -if [ "${VERSION}" != "LATEST" ] && [ "${VERSION}" != "1.17" ] && [ "${VERSION}" != "PURPUR" ] ; then - log "ERROR: Airplane server type only supports VERSION=LATEST, VERSION=1.17, VERSION=PURPUR. Note that these are branches, not #.#.# versions." +if [ "${VERSION}" != "LATEST" ] && [ "${VERSION}" != "PURPUR" ] ; then + log "ERROR: Airplane server type only supports VERSION=LATEST, VERSION=PURPUR." exit 1 fi -: ${AIRPLANE_BUILD:=lastSuccessfulBuild} : ${AIRPLANE_TYPE:=airplane} -if [ "${VERSION}" = "LATEST" ] || [ "${VERSION}" = "1.17" ]; then - AIRPLANE_BRANCH="1.17" +if [ "${VERSION}" = "LATEST" ] ; then + AIRPLANE_TYPE="airplane" fi if [ "${VERSION}" = "PURPUR" ]; then - AIRPLANE_BRANCH="Purpur-1.17" AIRPLANE_TYPE="airplanepurpur" fi -log "Using Airplane-${AIRPLANE_BRANCH} branch" +log "Using ${AIRPLANE_TYPE} 1.17.1 (1.18 unsupported - use Paper/Pufferfish/Purpur for newer versions)" -export SERVER=airplane-${AIRPLANE_BRANCH}-${AIRPLANE_BUILD}.jar +export SERVER=${AIRPLANE_TYPE}-1.17.1.jar log "Removing old Airplane versions ..." shopt -s nullglob -for f in airplane-*.jar; do +for f in airplane*.jar; do [[ $f != $SERVER ]] && rm $f done if [ ! -f "$SERVER" ] || isTrue "${FORCE_REDOWNLOAD:-false}"; then - downloadUrl="https://ci.tivy.ca/job/Airplane-${AIRPLANE_BRANCH}/${AIRPLANE_BUILD}/artifact/launcher-${AIRPLANE_TYPE}.jar" + downloadUrl="https://airplane.gg/dl/launcher-${AIRPLANE_TYPE}1.17.1.jar" log "Downloading Airplane from $downloadUrl ..." if ! get -o "$SERVER" "$downloadUrl"; then log "ERROR: failed to download from $downloadUrl (status=$?)" diff --git a/scripts/start-setupWorld b/scripts/start-setupWorld index db14635..dc3901e 100755 --- a/scripts/start-setupWorld +++ b/scripts/start-setupWorld @@ -5,7 +5,7 @@ set -e isDebugging && set -x -if [ $TYPE = "CURSEFORGE" ]; then +if [ "$TYPE" = "CURSEFORGE" ]; then worldDest=$FTB_DIR/${LEVEL:-world} else worldDest=/data/${LEVEL:-world} @@ -19,19 +19,23 @@ if [[ "$WORLD" ]] && ( isTrue "${FORCE_WORLD_COPY}" || [ ! -d "$worldDest" ] ); "${worldDest}_the_end" fi - if isURL $WORLD; then - curl -fsSL "$WORLD" -o /tmp/world.zip - zipSrc=/tmp/world.zip - elif [[ "$WORLD" =~ .*\.zip ]]; then - zipSrc="$WORLD" + if isURL "$WORLD"; then + log "Downloading world from $WORLD" + if ! get -o /tmp/world.bin "$WORLD"; then + log "ERROR: failed to download world from $WORLD" + exit 1 + fi + WORLD=/tmp/world.bin fi - if [[ "$zipSrc" ]]; then - log "Unzipping world" + if [ -f "$WORLD" ]; then + log "Extracting world" # Stage contents so that the correct subdirectory can be picked off mkdir -p /tmp/world-data - (cd /tmp/world-data && unzip -o -q "$zipSrc") + if ! extract "$WORLD" /tmp/world-data; then + exit 1 + fi if [ "$FAMILY" = "SPIGOT" ]; then baseDirs=$(find /tmp/world-data -name "level.dat" -not -path "*_nether*" -not -path "*_the_end*" -exec dirname "{}" \;) @@ -39,6 +43,11 @@ if [[ "$WORLD" ]] && ( isTrue "${FORCE_WORLD_COPY}" || [ ! -d "$worldDest" ] ); baseDirs=$(find /tmp/world-data -name "level.dat" -exec dirname "{}" \;) fi + if ! [[ $baseDirs ]]; then + log "ERROR world content is not valid since level.dat could not be found" + exit 2 + fi + count=$(echo "$baseDirs" | wc -l) if [[ $count -gt 1 ]]; then baseDir="$(echo "$baseDirs" | sed -n ${WORLD_INDEX:-1}p)" @@ -56,9 +65,12 @@ if [[ "$WORLD" ]] && ( isTrue "${FORCE_WORLD_COPY}" || [ ! -d "$worldDest" ] ); [ -d "${baseDir}_nether" ] && rsync --remove-source-files --recursive --delete "${baseDir}_nether/" "${worldDest}_nether" [ -d "${baseDir}_the_end" ] && rsync --remove-source-files --recursive --delete "${baseDir}_the_end/" "${worldDest}_the_end" fi - else + elif [ -d "$WORLD" ]; then log "Cloning world directory from $WORLD ..." rsync --recursive --delete "${WORLD%/}"/ "$worldDest" + else + log "ERROR: world file/directory $WORLD is missing" + exit 1 fi if [ "$FAMILY" = "SPIGOT" ]; then diff --git a/scripts/start-utils b/scripts/start-utils index 0861e24..a2da3d5 100755 --- a/scripts/start-utils +++ b/scripts/start-utils @@ -188,4 +188,19 @@ function isType() { fi done return 1 +} + +function extract() { + src=${1?} + destDir=${2?} + + type=$(file -b --mime-type "${src}") + if [[ $type == application/zip ]]; then + unzip -q -d "${destDir}" "${src}" + elif [[ $type == application/x-tar ]]; then + tar -C "${destDir}" -xf "${src}" + else + log "ERROR: unsupported archive type: $type" + return 1 + fi } \ No newline at end of file diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..adbb97d --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +data/ \ No newline at end of file diff --git a/tests/setuponlytests/forgeapimods_file/docker-compose.yml b/tests/setuponlytests/forgeapimods_file/docker-compose.disabled.yml similarity index 100% rename from tests/setuponlytests/forgeapimods_file/docker-compose.yml rename to tests/setuponlytests/forgeapimods_file/docker-compose.disabled.yml diff --git a/tests/setuponlytests/forgeapimods_projectids/docker-compose.yml b/tests/setuponlytests/forgeapimods_projectids/docker-compose.disabled.yml similarity index 100% rename from tests/setuponlytests/forgeapimods_projectids/docker-compose.yml rename to tests/setuponlytests/forgeapimods_projectids/docker-compose.disabled.yml diff --git a/tests/setuponlytests/generic-packs/docker-compose.yml b/tests/setuponlytests/generic-packs/docker-compose.yml index 46dd834..071c430 100644 --- a/tests/setuponlytests/generic-packs/docker-compose.yml +++ b/tests/setuponlytests/generic-packs/docker-compose.yml @@ -9,6 +9,4 @@ services: GENERIC_PACKS: https://github.com/itzg/mc-image-helper/releases/download/v1.9.5/mc-image-helper-1.9.5.zip,/packs/testing.zip volumes: - ./packs:/packs - - data:/data -volumes: - data: {} + - ./data:/data diff --git a/tests/setuponlytests/generic-packs/verify.sh b/tests/setuponlytests/generic-packs/verify.sh new file mode 100644 index 0000000..15166fe --- /dev/null +++ b/tests/setuponlytests/generic-packs/verify.sh @@ -0,0 +1 @@ +mc-image-helper assert fileExists one.txt mods/two.txt \ No newline at end of file diff --git a/tests/setuponlytests/test.sh b/tests/setuponlytests/test.sh index 169cf0b..8c8cb38 100644 --- a/tests/setuponlytests/test.sh +++ b/tests/setuponlytests/test.sh @@ -1,43 +1,50 @@ #!/bin/bash +set -euo pipefail +IFS=$'\n\t' # go to script root directory cd "$(dirname "$0")" || exit 1 -# compose down function for reuse -down() { - docker-compose down -v --remove-orphans -} - -checkandExitOnFailure(){ - failed=$1 - # docker-compose logs outputs messages from the specified container - if $failed; then - docker-compose logs mc - down - cd .. - exit 2 - fi -} - -# tests that only run the setup files for things like downloads and configuration. +# tests that only run the setup files for things like downloads and configuration. setupOnlyMinecraftTest(){ folder=$1 cd "$folder" - failed=false - # run the monitor to validate the Minecraft image is healthy - docker-compose --log-level ERROR up --quiet-pull --exit-code-from mc 2>/dev/null || failed=true - echo "${folder} Result: failed=$failed" - checkandExitOnFailure $failed - down + result=0 + + if ! logs=$(docker compose run --quiet-pull mc 2>&1); then + echo "${folder} setup FAILED" + echo ":::::::::::: LOGS :::::::::::::::: +$logs +:::::::::::::::::::::::::::::::::: +" + result=1 + elif [ -f verify.sh ]; then + if ! docker run --rm --entrypoint bash -v "${PWD}/data":/data -v "${PWD}/verify.sh":/verify "${IMAGE_TO_TEST:-itzg/minecraft-server}" /verify; then + echo "${folder} verify FAILED" + result=1 + else + echo "${folder} verify PASS" + fi + else + echo "${folder} PASS" + fi + + docker compose down -v --remove-orphans cd .. + + return $result } # go through each folder in setuponly and test setups -FOLDERS=$(ls) -for folder in $FOLDERS; do - # If folder is a directory - if [ -d "$folder" ]; then - echo "Starting Tests on ${folder}" - setupOnlyMinecraftTest $folder - fi -done +if (( $# > 0 )); then + for folder in "$@"; do + echo "Starting Tests in ${folder}" + setupOnlyMinecraftTest "$folder" + done +else + readarray -t folders < <(find . -maxdepth 2 -mindepth 2 -name docker-compose.yml -printf '%h\n') + for folder in "${folders[@]}"; do + echo "Starting Tests in ${folder}" + setupOnlyMinecraftTest "$folder" + done +fi diff --git a/tests/setuponlytests/world_from_tgz/docker-compose.yml b/tests/setuponlytests/world_from_tgz/docker-compose.yml new file mode 100644 index 0000000..530b1ed --- /dev/null +++ b/tests/setuponlytests/world_from_tgz/docker-compose.yml @@ -0,0 +1,14 @@ +version: "3" + +services: + mc: + restart: "no" + image: ${IMAGE_TO_TEST:-itzg/minecraft-server} + environment: + EULA: "TRUE" + SETUP_ONLY: "TRUE" + VERSION: ${MINECRAFT_VERSION:-LATEST} + WORLD: /worlds/world-for-testing.tgz + volumes: + - ./worlds:/worlds:ro + - ./data:/data diff --git a/tests/setuponlytests/world_from_tgz/verify.sh b/tests/setuponlytests/world_from_tgz/verify.sh new file mode 100644 index 0000000..b2138d0 --- /dev/null +++ b/tests/setuponlytests/world_from_tgz/verify.sh @@ -0,0 +1 @@ +mc-image-helper assert fileExists world/level.dat \ No newline at end of file diff --git a/tests/setuponlytests/world_from_tgz/worlds/world-for-testing.tgz b/tests/setuponlytests/world_from_tgz/worlds/world-for-testing.tgz new file mode 100644 index 0000000..6f51bb8 Binary files /dev/null and b/tests/setuponlytests/world_from_tgz/worlds/world-for-testing.tgz differ diff --git a/tests/setuponlytests/world_from_zip/docker-compose.yml b/tests/setuponlytests/world_from_zip/docker-compose.yml new file mode 100644 index 0000000..4a30460 --- /dev/null +++ b/tests/setuponlytests/world_from_zip/docker-compose.yml @@ -0,0 +1,14 @@ +version: "3" + +services: + mc: + restart: "no" + image: ${IMAGE_TO_TEST:-itzg/minecraft-server} + environment: + EULA: "TRUE" + SETUP_ONLY: "TRUE" + VERSION: ${MINECRAFT_VERSION:-LATEST} + WORLD: /worlds/world-for-testing.zip + volumes: + - ./worlds:/worlds:ro + - ./data:/data diff --git a/tests/setuponlytests/world_from_zip/verify.sh b/tests/setuponlytests/world_from_zip/verify.sh new file mode 100644 index 0000000..b2138d0 --- /dev/null +++ b/tests/setuponlytests/world_from_zip/verify.sh @@ -0,0 +1 @@ +mc-image-helper assert fileExists world/level.dat \ No newline at end of file diff --git a/tests/setuponlytests/world_from_zip/worlds/world-for-testing.zip b/tests/setuponlytests/world_from_zip/worlds/world-for-testing.zip new file mode 100644 index 0000000..88fd182 Binary files /dev/null and b/tests/setuponlytests/world_from_zip/worlds/world-for-testing.zip differ diff --git a/tests/test.sh b/tests/test.sh index a538ddc..0aa8606 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -1,18 +1,15 @@ #!/bin/bash +set -euo pipefail +IFS=$'\n\t' # go to script root directory cd "$(dirname "$0")" || exit 1 # go through top level folders and trigger the tests in the subfolders -FOLDERS=$(ls) -for folder in $FOLDERS; do - # If folder is a directory - if [ -d "$folder" ]; then - cd "$folder" - if [ -f "./test.sh" ]; then - echo "Starting ${folder} Tests" - sh ./test.sh - fi - cd .. - fi +readarray -t folders < <(find . -maxdepth 2 -mindepth 2 -name test.sh -printf '%h\n') +for folder in "${folders[@]}"; do + cd "$folder" + echo "Starting ${folder} Tests" + bash ./test.sh + cd .. done