Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,38 @@ if [ "$NEEDS_INSTALL" = true ]; then
echo "RoonServer installed successfully."
fi

# --- Patch start.sh for in-container update durability ---------------------
# RoonServer's start.sh handles in-container upgrades by extracting a new
# build tarball into RoonServer/.update.tmp/. The bundled script uses bare
# `tar xf "$PKG"` (no --no-same-owner / --no-same-permissions), and the
# build tarballs store files as uid 1001 (the Azure Pipelines build agent).
# That combination fails on hosts with restricted chown semantics — userns-
# remapped Docker daemons, NFS mounts with root_squash, TrueNAS bind
# mounts with strict ACLs — because tar tries to chown extracted files
# to a uid the host filesystem rejects, exits non-zero, and start.sh's
# `|| doerror` catches the failure as "ERROR: failed to tar xf $PKG".
# Result: initial install succeeds (entrypoint extract uses the right
# flags); auto-updates silently fail.
#
# Fix the running on-disk start.sh to mirror our entrypoint extract flags.
# Idempotent: skip when the flag is already present (avoids double-patches
# on restart, and makes upstream's eventual fix a no-op here). Re-runs on
# every container start so each upgrade — which replaces start.sh with
# the unpatched upstream version — gets re-patched on the next restart.

START_SH="${ROON_APP_DIR}/RoonServer/start.sh"
# Guard checks for BOTH flags so a partial upstream fix (e.g., owner added
# but not permissions) still triggers the full patch attempt. Post-sed we
# also verify both flags are present before logging success.
if [ -f "$START_SH" ] && ! { grep -q -- "--no-same-owner" "$START_SH" \
&& grep -q -- "--no-same-permissions" "$START_SH"; }; then
if sed -i 's|tar xf "$PKG"|tar xf --no-same-owner --no-same-permissions "$PKG"|' "$START_SH" \
&& grep -q -- "--no-same-owner" "$START_SH" \
&& grep -q -- "--no-same-permissions" "$START_SH"; then
echo "Patched RoonServer/start.sh for in-container update compatibility."
fi
fi

# --- Final state log --------------------------------------------------------
# Line format is contract-ish: runtime tests grep for "^Branch: production"
# and "^Branch: earlyaccess". Don't change the prefix or spacing without
Expand Down
73 changes: 73 additions & 0 deletions tests/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,79 @@ check "multi-line VERSION: last line wins (detects production)" \
check "image has no USER directive (runs as root)" \
sh -c '[ -z "$(docker inspect --format "{{.Config.User}}" "$1")" ]' _ "$IMAGE"

# In-container update compatibility: entrypoint must patch start.sh's bare
# `tar xf` to use --no-same-owner --no-same-permissions, otherwise upgrades
# fail on userns-remap / NFS-root_squash / strict-ACL hosts because tar
# tries to chown extracted files to the build-agent uid (1001) and exits
# non-zero on chown failure.
#
# Pre-seed a fake RoonServer install with start.sh containing the upstream
# bare-tar pattern, plus a Server/RoonServer launcher and VERSION file so
# the entrypoint takes the "no reinstall needed" path. Replace the
# entrypoint's exec target with `:` (the no-op shell builtin) by also
# providing a fake start.sh that just exits — but we want to inspect
# start.sh after the patch step, so we use --entrypoint sh to bypass
# entrypoint.sh entirely on the second run and just read the file.
PATCH_TMP=$(mktemp -d)
docker run --rm --entrypoint sh -v "$PATCH_TMP:/Roon" "$IMAGE" -c '
mkdir -p /Roon/app/RoonServer/Server
cat > /Roon/app/RoonServer/start.sh <<'\''EOF'\''
#!/bin/bash
if [ "$1" = "--update" ]; then
PKG="$2"; ROOTDIR="$3"
tar xf "$PKG" -C "$ROOTDIR/.update.tmp"
fi
exit 0
EOF
chmod +x /Roon/app/RoonServer/start.sh
touch /Roon/app/RoonServer/Server/RoonServer
printf "100\nproduction\n" > /Roon/app/RoonServer/VERSION
' >/dev/null

# Run the entrypoint — should detect existing install (no reinstall),
# patch start.sh, then exec it (which exits 0 thanks to our fake script).
PATCH_LOG=$(docker run --rm -e ROON_INSTALL_BRANCH=production -v "$PATCH_TMP:/Roon" "$IMAGE" 2>&1) || true

# Read the (possibly-patched) start.sh from inside the container so the
# test doesn't depend on host file-sharing config (Docker Desktop on
# macOS doesn't share /var/folders by default).
PATCHED_SH=$(docker run --rm --entrypoint cat -v "$PATCH_TMP:/Roon" "$IMAGE" /Roon/app/RoonServer/start.sh 2>&1)
rm -rf "$PATCH_TMP" 2>/dev/null || true

check "start.sh patch: tar invocation gets --no-same-owner + --no-same-permissions" \
sh -c 'echo "$1" | grep -qF "tar xf --no-same-owner --no-same-permissions \"\$PKG\""' _ "$PATCHED_SH"
check "start.sh patch: idempotent (only one set of flags after patching)" \
sh -c '[ "$(echo "$1" | grep -c -- --no-same-owner)" -eq 1 ]' _ "$PATCHED_SH"
check "start.sh patch: log message is emitted on patching" \
sh -c 'echo "$1" | grep -q "Patched RoonServer/start.sh"' _ "$PATCH_LOG"

# Idempotency check on a second run: PATCH_TMP was deleted, so re-seed
# with an already-patched start.sh and verify the entrypoint is a no-op.
PATCH_TMP2=$(mktemp -d)
docker run --rm --entrypoint sh -v "$PATCH_TMP2:/Roon" "$IMAGE" -c '
mkdir -p /Roon/app/RoonServer/Server
cat > /Roon/app/RoonServer/start.sh <<'\''EOF'\''
#!/bin/bash
if [ "$1" = "--update" ]; then
tar xf --no-same-owner --no-same-permissions "$PKG" -C "$ROOTDIR/.update.tmp"
fi
exit 0
EOF
chmod +x /Roon/app/RoonServer/start.sh
touch /Roon/app/RoonServer/Server/RoonServer
printf "100\nproduction\n" > /Roon/app/RoonServer/VERSION
' >/dev/null
PATCH2_EXIT=0
PATCH2_LOG=$(docker run --rm -e ROON_INSTALL_BRANCH=production -v "$PATCH_TMP2:/Roon" "$IMAGE" 2>&1) || PATCH2_EXIT=$?
rm -rf "$PATCH_TMP2" 2>/dev/null || true

# Guard the negative assertion below: if the container failed to start
# at all, the "no log" assertion would pass for the wrong reason.
check "start.sh patch: idempotent run exits cleanly" \
test "$PATCH2_EXIT" -eq 0
check "start.sh patch: skipped (no log) when both flags already present" \
sh -c '! echo "$1" | grep -q "Patched RoonServer/start.sh"' _ "$PATCH2_LOG"

# Startup banner is always emitted (regardless of branch resolution outcome)
check "startup banner always logged" \
sh -c 'echo "$1" | grep -q "^Roon Docker image "' _ "$UNSET_OUTPUT"
Expand Down
Loading