From d6d0582dfc085c3426fd145d6e08857578ab8c04 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 14:33:45 +0000 Subject: [PATCH 1/2] chore(ark-survival-evolved): drop the Gameplane-built ARK image The ark-survival-evolved module now runs the community image hermsi/ark-server (GameplanePanel/module#58), so stop building our own. - Remove containers/games/ark-survival-evolved and its images.yaml matrix entry and README tree line. - Point the two hand-run ARK e2e tests at hermsi/ark-server: mount /app instead of /data, ADMIN_PASSWORD instead of RCON_PASSWORD, SERVER_PASSWORD="" (image default would lock the bot out), raw UDP port 7778, and a /cluster extra volume for the cluster marker. Test edits signed off by valgul on 2026-11-11. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Claude --- .github/workflows/images.yaml | 2 - containers/README.md | 3 - .../games/ark-survival-evolved/Dockerfile | 30 -------- .../games/ark-survival-evolved/entrypoint.sh | 68 ------------------- ...rksurvival_cluster_persistence_e2e_test.go | 9 ++- test/e2e/arksurvivalevolved_bot_e2e_test.go | 10 +-- 6 files changed, 12 insertions(+), 110 deletions(-) delete mode 100644 containers/games/ark-survival-evolved/Dockerfile delete mode 100644 containers/games/ark-survival-evolved/entrypoint.sh diff --git a/.github/workflows/images.yaml b/.github/workflows/images.yaml index 14d6b0d47..aceff56f1 100644 --- a/.github/workflows/images.yaml +++ b/.github/workflows/images.yaml @@ -207,8 +207,6 @@ jobs: context: containers/games/squad - game: the-isle context: containers/games/the-isle - - game: ark-survival-evolved - context: containers/games/ark-survival-evolved - game: mount-and-blade-2-bannerlord context: containers/games/mount-and-blade-2-bannerlord - game: fivem diff --git a/containers/README.md b/containers/README.md index a5967fc85..b56e143f2 100644 --- a/containers/README.md +++ b/containers/README.md @@ -28,9 +28,6 @@ containers/ ├── the-isle/ │ ├── Dockerfile │ └── entrypoint.sh - ├── ark-survival-evolved/ - │ ├── Dockerfile - │ └── entrypoint.sh ├── mount-and-blade-2-bannerlord/ │ ├── Dockerfile │ └── entrypoint.sh diff --git a/containers/games/ark-survival-evolved/Dockerfile b/containers/games/ark-survival-evolved/Dockerfile deleted file mode 100644 index 4fdb4ee77..000000000 --- a/containers/games/ark-survival-evolved/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -# Gameplane game server image for ARK: Survival Evolved (dedicated server). -# -# RCON uses ServerAdminPassword, which the entrypoint sets from RCON_PASSWORD. -# -# Builds on the shared SteamCMD base (containers/common/steamcmd/). The game is -# installed at container start by steam-install.sh into STEAM_INSTALL_DIR -# (default /data, the GameServer's persistent volume). No game files are -# baked into the image. - -# CI passes: --build-arg STEAMCMD_BASE_IMAGE=ghcr.io/gameplanepanel/gameplane/common-steamcmd@sha256:... -ARG STEAMCMD_BASE_IMAGE=gameplane-steamcmd-base:latest -FROM ${STEAMCMD_BASE_IMAGE} - -# tini (-g) forwards SIGTERM to the whole process group, so the server gets -# it even when it is started through a vendor wrapper script. -USER root -RUN apt-get update && \ - apt-get install -y --no-install-recommends tini=0.19.0-6 && \ - rm -rf /var/lib/apt/lists/* -USER gameserver:gameserver - -COPY --chmod=0755 entrypoint.sh /entrypoint.sh - -ENTRYPOINT ["/usr/bin/tini", "-g", "--", "/entrypoint.sh"] - -ARG VERSION=dev -LABEL org.opencontainers.image.version="${VERSION}" -LABEL org.opencontainers.image.title="Gameplane ARK: Survival Evolved Server" -LABEL org.opencontainers.image.description="ARK: Survival Evolved dedicated server for Gameplane" -LABEL org.opencontainers.image.source="https://github.com/gameplanepanel/gameplane" diff --git a/containers/games/ark-survival-evolved/entrypoint.sh b/containers/games/ark-survival-evolved/entrypoint.sh deleted file mode 100644 index 39a2fbdbf..000000000 --- a/containers/games/ark-survival-evolved/entrypoint.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Gameplane entrypoint for ARK: Survival Evolved dedicated server. -# Installs/updates the game into STEAM_INSTALL_DIR, writes the settings -# Gameplane manages from env vars, then execs the server. - -STEAM_INSTALL_DIR="${STEAM_INSTALL_DIR:-/data}" -UPDATE_ON_BOOT="${UPDATE_ON_BOOT:-true}" -STEAM_VALIDATE="${STEAM_VALIDATE:-false}" - -# set_kv KEY VALUE FILE: replace the first "KEY=..." line in FILE, or append -# "KEY=VALUE" when the key is missing. VALUE is escaped for sed. -set_kv() { - local key="$1" value="$2" file="$3" escaped - escaped="$(printf '%s' "$value" | sed -e 's/[\\|&]/\\&/g')" - if grep -q "^${key}=" "$file"; then - sed -i "0,/^${key}=.*/s|^${key}=.*|${key}=${escaped}|" "$file" - else - printf '%s=%s\n' "$key" "$value" >> "$file" - fi -} - -if [[ "$UPDATE_ON_BOOT" == "true" ]]; then - STEAM_SKIP_IF_INSTALLED="false" -else - STEAM_SKIP_IF_INSTALLED="true" -fi - -STEAM_APPID="376030" \ -STEAM_INSTALL_DIR="${STEAM_INSTALL_DIR}" \ -STEAM_VALIDATE="${STEAM_VALIDATE}" \ -STEAM_SKIP_IF_INSTALLED="${STEAM_SKIP_IF_INSTALLED}" \ -STEAM_SENTINEL_FILE="${STEAM_INSTALL_DIR}/ShooterGame/Binaries/Linux/ShooterGameServer" \ - steam-install.sh - -if [[ ! -f "${STEAM_INSTALL_DIR}/ShooterGame/Binaries/Linux/ShooterGameServer" ]]; then echo "error: game binary missing after install: ${STEAM_INSTALL_DIR}/ShooterGame/Binaries/Linux/ShooterGameServer"; exit 1; fi - -PORT="${PORT:-7777}" -QUERY_PORT="${QUERY_PORT:-27015}" -RCON_PORT="${RCON_PORT:-27020}" -MAP_NAME="${MAP_NAME:-TheIsland}" - -# ARK opens many files at once; raise the soft limit as far as allowed. -ulimit -n "$(ulimit -Hn)" 2>/dev/null || true - -QUERY="${MAP_NAME}?listen" -QUERY+="?SessionName=${SESSION_NAME:-Gameplane ARK Server}" -QUERY+="?MaxPlayers=${MAX_PLAYERS:-70}" -QUERY+="?Port=${PORT}?QueryPort=${QUERY_PORT}" -if [[ -n "${SERVER_PASSWORD:-}" ]]; then - QUERY+="?ServerPassword=${SERVER_PASSWORD}" -fi -if [[ -n "${RCON_PASSWORD:-}" ]]; then - QUERY+="?ServerAdminPassword=${RCON_PASSWORD}?RCONEnabled=True?RCONPort=${RCON_PORT}" -else - echo "warning: RCON_PASSWORD is empty; ARK RCON stays disabled" -fi - -ARGS=("${QUERY}" -server -log -NoBattlEye) -if [[ -n "${CLUSTER_ID:-}" ]]; then - mkdir -p "${STEAM_INSTALL_DIR}/ShooterGame/Saved/clusters" - ARGS+=("-clusterid=${CLUSTER_ID}" "-ClusterDirOverride=${STEAM_INSTALL_DIR}/ShooterGame/Saved/clusters") -fi - -cd "${STEAM_INSTALL_DIR}/ShooterGame/Binaries/Linux" -echo "ARK entrypoint: launching ${MAP_NAME} (port ${PORT}, query ${QUERY_PORT}, rcon ${RCON_PORT})" -exec ./ShooterGameServer "${ARGS[@]}" diff --git a/test/e2e/arksurvival_cluster_persistence_e2e_test.go b/test/e2e/arksurvival_cluster_persistence_e2e_test.go index f98389711..2e47ccab1 100644 --- a/test/e2e/arksurvival_cluster_persistence_e2e_test.go +++ b/test/e2e/arksurvival_cluster_persistence_e2e_test.go @@ -31,7 +31,7 @@ func TestGameServer_ArkSurvival_ClusterPersistence(t *testing.T) { uid := time.Now().UnixNano() % 100000 tmplName := fmt.Sprintf("e2e-ark-cluster-%d", uid) gsName := fmt.Sprintf("ark-shard-01-%d", uid) - const mountPath = "/data" + const mountPath = "/app" clusterID := fmt.Sprintf("cluster-%d", uid) marker := fmt.Sprintf("ark-survivor-payload-%d", uid) @@ -43,7 +43,7 @@ func TestGameServer_ArkSurvival_ClusterPersistence(t *testing.T) { "displayName": "E2E ARK Cluster Shard", "game": "ark-survival-evolved", "version": "1.0.0", - "image": "ghcr.io/gameplanepanel/gameplane/ark-survival-evolved:latest@sha256:2a4298f8055dbff4107c49c961cace1f2bed42809e6b93fb4924371ae4484e58", + "image": "hermsi/ark-server:latest@sha256:ce291990613e76f7019be31b96fe247188ce4957724c913c0e30097042c44bf5", "ports": []any{ map[string]any{"name": "game", "containerPort": int64(7777), "protocol": "UDP", "advertise": true}, map[string]any{"name": "query", "containerPort": int64(27015), "protocol": "UDP", "advertise": true}, @@ -52,6 +52,9 @@ func TestGameServer_ArkSurvival_ClusterPersistence(t *testing.T) { "storage": map[string]any{ "size": "35Gi", "mountPath": mountPath, + "extra": []any{ + map[string]any{"name": "cluster", "mountPath": "/cluster", "size": "1Gi"}, + }, }, "resources": map[string]any{ "requests": map[string]any{"cpu": "2", "memory": "8Gi"}, @@ -105,7 +108,7 @@ func TestGameServer_ArkSurvival_ClusterPersistence(t *testing.T) { waitPVCBound(t, ns, gsName+"-data", 2*time.Minute) waitGameContainerReady(t, ns, gsName+"-0", 5*time.Minute) - clusterDir := fmt.Sprintf("%s/ShooterGame/Saved/clusters/%s", mountPath, clusterID) + clusterDir := fmt.Sprintf("/cluster/%s", clusterID) markerFile := fmt.Sprintf("%s/survivor.dat", clusterDir) writeCmd := fmt.Sprintf("mkdir -p %s && echo -n %s > %s && sync", clusterDir, marker, markerFile) diff --git a/test/e2e/arksurvivalevolved_bot_e2e_test.go b/test/e2e/arksurvivalevolved_bot_e2e_test.go index 5333011e4..64f5258ef 100644 --- a/test/e2e/arksurvivalevolved_bot_e2e_test.go +++ b/test/e2e/arksurvivalevolved_bot_e2e_test.go @@ -25,18 +25,20 @@ func TestGameServer_ArkSurvivalEvolvedBot_Query(t *testing.T) { Game: "ark-survival-evolved", Template: fmt.Sprintf("e2e-ase-bot-%d", time.Now().UnixNano()), DisplayName: "E2E ARK: Survival Evolved", - Image: "ghcr.io/gameplanepanel/gameplane/ark-survival-evolved:latest@sha256:2a4298f8055dbff4107c49c961cace1f2bed42809e6b93fb4924371ae4484e58", + Image: "hermsi/ark-server:latest@sha256:ce291990613e76f7019be31b96fe247188ce4957724c913c0e30097042c44bf5", Env: map[string]string{ - "SESSION_NAME": "Gameplane E2E ASE", - "RCON_PASSWORD": "secret-rcon-password", + "SESSION_NAME": "Gameplane E2E ASE", + "ADMIN_PASSWORD": "secret-rcon-password", + "SERVER_PASSWORD": "", }, Ports: []gamePort{ {Name: "game", Port: 7777, Protocol: "UDP"}, + {Name: "raw", Port: 7778, Protocol: "UDP"}, {Name: "query", Port: 27015, Protocol: "UDP"}, {Name: "rcon", Port: 27020, Protocol: "TCP"}, }, StorageSize: "30Gi", - MountPath: "/data", + MountPath: "/app", Resources: gameResources{ ReqCPU: "2", ReqMem: "8Gi", LimCPU: "4", LimMem: "16Gi", From 358a4851f5225b28946e1c6c83aa7aaf8dd0ce6a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 18:12:38 +0000 Subject: [PATCH 2/2] test(e2e): fix and tighten the ARK Evolved tests CodeRabbit review on #915, each change signed off by valgul 2026-11-11: - Cluster test: declare CLUSTER_ID in the template configSchema; the operator rejected it as an unknown config key, so the test never got past GameServer creation. - Cluster test: assert ShooterGameServer runs with -ClusterDirOverride=/cluster and the test cluster id. - Bot test: map the operator RCON password to ADMIN_PASSWORD via passwordEnv and add a Path A RCON check (ListPlayers -> "No Players Connected") so a password-mapping failure is caught. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Claude --- ...rksurvival_cluster_persistence_e2e_test.go | 18 ++++++++++++++ test/e2e/arksurvivalevolved_bot_e2e_test.go | 24 ++++++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/test/e2e/arksurvival_cluster_persistence_e2e_test.go b/test/e2e/arksurvival_cluster_persistence_e2e_test.go index 2e47ccab1..39e32a7be 100644 --- a/test/e2e/arksurvival_cluster_persistence_e2e_test.go +++ b/test/e2e/arksurvival_cluster_persistence_e2e_test.go @@ -44,6 +44,9 @@ func TestGameServer_ArkSurvival_ClusterPersistence(t *testing.T) { "game": "ark-survival-evolved", "version": "1.0.0", "image": "hermsi/ark-server:latest@sha256:ce291990613e76f7019be31b96fe247188ce4957724c913c0e30097042c44bf5", + "configSchema": []any{ + map[string]any{"name": "CLUSTER_ID", "type": "string"}, + }, "ports": []any{ map[string]any{"name": "game", "containerPort": int64(7777), "protocol": "UDP", "advertise": true}, map[string]any{"name": "query", "containerPort": int64(27015), "protocol": "UDP", "advertise": true}, @@ -108,6 +111,21 @@ func TestGameServer_ArkSurvival_ClusterPersistence(t *testing.T) { waitPVCBound(t, ns, gsName+"-data", 2*time.Minute) waitGameContainerReady(t, ns, gsName+"-0", 5*time.Minute) + // The marker below only proves the /cluster volume persists. Check that + // the ARK server itself was started against it: hermsi/ark-server passes + // -ClusterDirOverride=/cluster and -clusterid once CLUSTER_ID is set. The + // server starts only after SteamCMD has installed ~25GB, hence the wait. + envInstance.Eventually(t, 45*time.Minute, func() (bool, string) { + out, err := envInstance.KubectlExec(t, ns, "pod/"+gsName+"-0", "pgrep", "-af", "ShooterGameServer") + if err != nil { + return false, "ShooterGameServer not running yet: " + err.Error() + " out=" + out + } + if !strings.Contains(out, "ClusterDirOverride=/cluster") || !strings.Contains(out, "clusterid="+clusterID) { + return false, fmt.Sprintf("ShooterGameServer command line lacks the /cluster override or cluster id %q: %s", clusterID, out) + } + return true, "" + }) + clusterDir := fmt.Sprintf("/cluster/%s", clusterID) markerFile := fmt.Sprintf("%s/survivor.dat", clusterDir) diff --git a/test/e2e/arksurvivalevolved_bot_e2e_test.go b/test/e2e/arksurvivalevolved_bot_e2e_test.go index 64f5258ef..81236ed92 100644 --- a/test/e2e/arksurvivalevolved_bot_e2e_test.go +++ b/test/e2e/arksurvivalevolved_bot_e2e_test.go @@ -28,7 +28,6 @@ func TestGameServer_ArkSurvivalEvolvedBot_Query(t *testing.T) { Image: "hermsi/ark-server:latest@sha256:ce291990613e76f7019be31b96fe247188ce4957724c913c0e30097042c44bf5", Env: map[string]string{ "SESSION_NAME": "Gameplane E2E ASE", - "ADMIN_PASSWORD": "secret-rcon-password", "SERVER_PASSWORD": "", }, Ports: []gamePort{ @@ -47,9 +46,28 @@ func TestGameServer_ArkSurvivalEvolvedBot_Query(t *testing.T) { ProbePort: 27015, ProbeDeadline: 4 * time.Minute, ExpectDepth: joindepth.QUERY, + // The operator injects its managed RCON password as ADMIN_PASSWORD, + // which hermsi/ark-server writes as ServerAdminPassword. Path A below + // proves the agent authenticates with it. RCON: map[string]any{ - "protocol": "source", - "port": int64(27020), + "protocol": "source", + "port": int64(27020), + "passwordEnv": "ADMIN_PASSWORD", + }, + ConsoleMode: "rcon", + Actions: []any{ + map[string]any{ + "id": "list-players", + "displayName": "List players", + "command": "ListPlayers", + }, + }, + // ARK answers ListPlayers on an empty server with "No Players + // Connected"; a broadcast (ServerChat) reply would be empty. + Control: pathAControl{ + Mode: "rcon", + Action: "list-players", + ExpectRaw: "No Players Connected", }, }) }