fix(docker): accept Unraid uid mappings (#38098)
Co-authored-by: Cornna <96944678+ymylive@users.noreply.github.com>
This commit is contained in:
@ -85,11 +85,12 @@ fi
|
||||
# is a no-op if the dir already exists. (#18482, salvages #18488)
|
||||
mkdir -p "$HERMES_HOME"
|
||||
|
||||
# Numeric UID/GID validation: must be digits only, 1000-65534
|
||||
# Numeric UID/GID validation: must be digits only, non-root, 1-65534.
|
||||
# NAS hosts such as Unraid commonly use low non-root IDs (99:100).
|
||||
validate_uid_gid() {
|
||||
case "$1" in
|
||||
''|*[!0-9]*) return 1 ;;
|
||||
*) [ "$1" -ge 1000 ] && [ "$1" -le 65534 ] ;;
|
||||
*) [ "$1" -ge 1 ] && [ "$1" -le 65534 ] ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@ -198,7 +199,7 @@ if [ "$needs_chown" = true ]; then
|
||||
# Hermes-owned subdirs: recursive chown is safe here because these are
|
||||
# created and managed exclusively by hermes (see the s6-setuidgid mkdir
|
||||
# -p block below for the canonical list).
|
||||
for sub in cron sessions logs hooks memories skills skins plans workspace home profiles; do
|
||||
for sub in cron sessions logs hooks memories skills skins plans workspace home profiles pairing platforms/pairing; do
|
||||
if [ -e "$HERMES_HOME/$sub" ]; then
|
||||
chown -R hermes:hermes "$HERMES_HOME/$sub" 2>/dev/null || \
|
||||
echo "[stage2] Warning: chown $HERMES_HOME/$sub failed (rootless container?) — continuing"
|
||||
@ -308,7 +309,9 @@ as_hermes mkdir -p \
|
||||
"$HERMES_HOME/skins" \
|
||||
"$HERMES_HOME/plans" \
|
||||
"$HERMES_HOME/workspace" \
|
||||
"$HERMES_HOME/home"
|
||||
"$HERMES_HOME/home" \
|
||||
"$HERMES_HOME/pairing" \
|
||||
"$HERMES_HOME/platforms/pairing"
|
||||
|
||||
# --- Install-method stamp (read by detect_install_method() in hermes status) ---
|
||||
# Preserved from the tini-era entrypoint (PR #27843). Must be written as
|
||||
|
||||
86
tests/tools/test_stage2_hook_unraid_uid.py
Normal file
86
tests/tools/test_stage2_hook_unraid_uid.py
Normal file
@ -0,0 +1,86 @@
|
||||
"""Regression tests for Docker stage2 UID/GID handling on NAS hosts.
|
||||
|
||||
Unraid commonly runs appdata as nobody:users (99:100). The stage2 hook must
|
||||
accept those non-root numeric IDs and keep legacy/new pairing stores writable
|
||||
after targeted ownership reconciliation.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
STAGE2_HOOK = REPO_ROOT / "docker" / "stage2-hook.sh"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def stage2_text() -> str:
|
||||
if not STAGE2_HOOK.exists():
|
||||
pytest.skip("docker/stage2-hook.sh not present in this checkout")
|
||||
return STAGE2_HOOK.read_text()
|
||||
|
||||
|
||||
def _uid_gid_validator(text: str) -> str:
|
||||
marker = "# --- UID/GID remap ---"
|
||||
before_marker = text.split(marker, 1)[0]
|
||||
start = before_marker.index("validate_uid_gid()")
|
||||
return before_marker[start:]
|
||||
|
||||
|
||||
def _validate_uid_gid(text: str, value: str) -> bool:
|
||||
bash = shutil.which("bash")
|
||||
if bash is None:
|
||||
pytest.skip("bash not available")
|
||||
script = _uid_gid_validator(text) + '\nvalidate_uid_gid "$CANDIDATE"\n'
|
||||
proc = subprocess.run(
|
||||
[bash, "-c", script],
|
||||
env={"PATH": os.environ.get("PATH", ""), "CANDIDATE": value},
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return proc.returncode == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["1", "99", "100", "1000", "65534"])
|
||||
def test_uid_gid_validator_accepts_non_root_nas_ids(stage2_text: str, value: str) -> None:
|
||||
assert _validate_uid_gid(stage2_text, value), (
|
||||
f"stage2 hook must accept NAS UID/GID {value}; Unraid uses 99:100 (#38070)"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["", "0", "abc", "99x", "65535"])
|
||||
def test_uid_gid_validator_rejects_root_invalid_and_out_of_range(
|
||||
stage2_text: str,
|
||||
value: str,
|
||||
) -> None:
|
||||
assert not _validate_uid_gid(stage2_text, value)
|
||||
|
||||
|
||||
def _targeted_chown_subdirs(text: str) -> list[str]:
|
||||
m = re.search(
|
||||
r"for sub in (?P<items>.*?); do\n\s*if \[ -e \"\$HERMES_HOME/\$sub\" \]",
|
||||
text,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert m, "stage2-hook.sh must contain the targeted subdir chown loop"
|
||||
return m.group("items").split()
|
||||
|
||||
|
||||
def test_targeted_chown_covers_legacy_and_new_pairing_dirs(stage2_text: str) -> None:
|
||||
subdirs = _targeted_chown_subdirs(stage2_text)
|
||||
assert "pairing" in subdirs
|
||||
assert "platforms/pairing" in subdirs
|
||||
|
||||
|
||||
def test_seeded_directory_list_covers_legacy_and_new_pairing_dirs(stage2_text: str) -> None:
|
||||
seed_block = stage2_text.split("as_hermes mkdir -p \\", 1)[1].split(
|
||||
"# --- Install-method stamp",
|
||||
1,
|
||||
)[0]
|
||||
assert '"$HERMES_HOME/pairing"' in seed_block
|
||||
assert '"$HERMES_HOME/platforms/pairing"' in seed_block
|
||||
Reference in New Issue
Block a user