fix: container volumes
Build and Publish Container / Publish Linux container (push) Successful in 4m4s
Build and Publish Container / Publish Linux container (push) Successful in 4m4s
This commit is contained in:
+7
-3
@@ -4,7 +4,7 @@ ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN dpkg --add-architecture i386 \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl lib32gcc-s1 python3 python3-pip python3-venv \
|
||||
ca-certificates curl gosu lib32gcc-s1 python3 python3-pip python3-venv \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& useradd --create-home --shell /bin/bash progression \
|
||||
&& mkdir -p /opt/steamcmd/steamapps /srv/progression/.data \
|
||||
@@ -19,9 +19,12 @@ RUN python3 -m venv /srv/venv \
|
||||
|
||||
COPY --chown=progression:progression progression_backend ./progression_backend
|
||||
COPY --chown=progression:progression art ./art
|
||||
USER progression
|
||||
COPY --chmod=755 docker-entrypoint.sh /usr/local/bin/progression-entrypoint
|
||||
|
||||
ENV PROGRESSION_HOST=0.0.0.0 \
|
||||
ENV HOME=/home/progression \
|
||||
USER=progression \
|
||||
XDG_DATA_HOME=/home/progression/.local/share \
|
||||
PROGRESSION_HOST=0.0.0.0 \
|
||||
PROGRESSION_PORT=8000 \
|
||||
PROGRESSION_DB_PATH=.data/progression.db \
|
||||
STEAMCMD_PATH=/opt/steamcmd/steamcmd.sh \
|
||||
@@ -35,4 +38,5 @@ EXPOSE 8000
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||
CMD curl -fsS http://127.0.0.1:8000/api/health || exit 1
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/progression-entrypoint"]
|
||||
CMD ["/srv/venv/bin/python", "-m", "progression_backend"]
|
||||
|
||||
@@ -19,4 +19,6 @@ On a new deployment, the hourly refresh timer stays paused while the first full
|
||||
Workshop download runs. Its hard limit defaults to 12 hours and can be changed
|
||||
with `INITIAL_DOWNLOAD_TIMEOUT_SECONDS`. Follow the per-mod title, Workshop ID,
|
||||
result, and completion percentage in Dockhand or with `docker compose logs -f`.
|
||||
Failed batches also include SteamCMD's output tail so network, subscription, and
|
||||
filesystem errors are visible directly in the container log.
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
prepare_volume() {
|
||||
path="$1"
|
||||
marker="$path/.progression-volume-ready"
|
||||
mkdir -p "$path"
|
||||
if [ ! -e "$marker" ]; then
|
||||
echo "Preparing persistent volume permissions: $path"
|
||||
chown -R progression:progression "$path"
|
||||
touch "$marker"
|
||||
chown progression:progression "$marker"
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_volume /srv/progression/.data
|
||||
prepare_volume /opt/steamcmd/steamapps
|
||||
|
||||
exec gosu progression "$@"
|
||||
@@ -293,6 +293,18 @@ class RefreshService:
|
||||
result = self.runner.run_batch(
|
||||
batch, timeout_seconds=remaining
|
||||
)
|
||||
if result.status != "completed":
|
||||
logger.warning(
|
||||
"SteamCMD pass %d batch %d/%d finished with "
|
||||
"status=%s exit_code=%s after %.1fs. Output:\n%s",
|
||||
pass_number,
|
||||
pass_batch_number,
|
||||
len(batches),
|
||||
result.status,
|
||||
result.exit_code,
|
||||
result.elapsed_seconds,
|
||||
result.output_tail or "(no SteamCMD output)",
|
||||
)
|
||||
self.database.add_download_attempt(
|
||||
{
|
||||
"run_id": run_id,
|
||||
|
||||
@@ -116,6 +116,20 @@ class SteamCmdRunner:
|
||||
def available(self) -> bool:
|
||||
return self.executable_path() is not None
|
||||
|
||||
def ensure_workshop_writable(self) -> None:
|
||||
workshop_dir = self.content_dir.parent.parent
|
||||
try:
|
||||
workshop_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.content_dir.mkdir(parents=True, exist_ok=True)
|
||||
probe = workshop_dir / ".progression-write-test"
|
||||
probe.write_text("ok", encoding="utf-8")
|
||||
probe.unlink()
|
||||
except OSError as error:
|
||||
raise PermissionError(
|
||||
f"SteamCMD Workshop directory is not writable: {workshop_dir}. "
|
||||
"Check the container volume ownership."
|
||||
) from error
|
||||
|
||||
def run_batch(
|
||||
self,
|
||||
workshop_ids: list[str],
|
||||
@@ -129,6 +143,7 @@ class SteamCmdRunner:
|
||||
invalid = [item for item in workshop_ids if not item.isdigit()]
|
||||
if invalid:
|
||||
raise ValueError(f"Invalid Workshop ids: {', '.join(invalid)}")
|
||||
self.ensure_workshop_writable()
|
||||
|
||||
command = [executable, "+login", "anonymous"]
|
||||
for workshop_id in workshop_ids:
|
||||
|
||||
+22
-1
@@ -23,7 +23,12 @@ from progression_backend.modlist import (
|
||||
)
|
||||
from progression_backend.refresh import RefreshCoordinator, RefreshService
|
||||
from progression_backend.steam_api import WorkshopDetails
|
||||
from progression_backend.workshop import SteamCmdBatchResult, parse_about_xml, utc_now
|
||||
from progression_backend.workshop import (
|
||||
SteamCmdBatchResult,
|
||||
SteamCmdRunner,
|
||||
parse_about_xml,
|
||||
utc_now,
|
||||
)
|
||||
|
||||
|
||||
class FakeSteam:
|
||||
@@ -176,6 +181,22 @@ class BackendTests(unittest.TestCase):
|
||||
self.assertEqual(parsed.package_id, "author.mod")
|
||||
self.assertEqual(parsed.display_name, "A & B")
|
||||
|
||||
def test_steamcmd_runner_prepares_writable_workshop_path(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
content_dir = (
|
||||
Path(directory)
|
||||
/ "steamapps"
|
||||
/ "workshop"
|
||||
/ "content"
|
||||
/ "294100"
|
||||
)
|
||||
runner = SteamCmdRunner("steamcmd", content_dir, 60)
|
||||
runner.ensure_workshop_writable()
|
||||
self.assertTrue(content_dir.is_dir())
|
||||
self.assertFalse(
|
||||
(content_dir.parent.parent / ".progression-write-test").exists()
|
||||
)
|
||||
|
||||
def test_rml_uses_workshop_id_and_escapes_names(self) -> None:
|
||||
payload = generate_rml(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user