diff --git a/furtka/updater.py b/furtka/updater.py index c78f488..6530211 100644 --- a/furtka/updater.py +++ b/furtka/updater.py @@ -319,6 +319,9 @@ def apply_update(tarball: Path, version: str) -> None: if target.exists(): shutil.rmtree(target) staging.rename(target) + # mktemp-style 700 default on the staging dir carries through the + # rename; Caddy (non-root) needs 755 to traverse /opt/furtka/current/. + target.chmod(0o755) write_state("swapping", latest=version) previous = None diff --git a/tests/test_updater.py b/tests/test_updater.py index 11edc2f..bc16d62 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -150,6 +150,10 @@ def test_apply_update_happy_path(tmp_path, updater, monkeypatch): assert current.resolve() == (versions / "26.1-alpha").resolve() assert (versions / "26.1-alpha" / "VERSION").read_text().strip() == "26.1-alpha" + # Version dir is 755 so Caddy can traverse it — the staging dir came + # from mktemp-ish extractall which defaults to 700, and carries through + # the rename unless we explicitly chmod. + assert (versions / "26.1-alpha").stat().st_mode & 0o777 == 0o755 # P1-2: Caddyfile was copied into /etc/caddy/ from the new version. assert updater._CADDYFILE_LIVE.read_text() == "# new caddy config\n" state = updater.read_state() diff --git a/tests/test_webinstaller_assets.py b/tests/test_webinstaller_assets.py index 42a1e4d..739ee7a 100644 --- a/tests/test_webinstaller_assets.py +++ b/tests/test_webinstaller_assets.py @@ -78,9 +78,23 @@ def test_resource_manager_extracts_to_versioned_slot(install_cmds): # An empty VERSION file must abort the install instead of silently # moving the staging dir into versions/ as a subdir. assert '[ -n "$ver" ]' in extract_cmd + # Version dir must be 755 so Caddy (non-root) can traverse it. + assert 'chmod 755 "/opt/furtka/versions/$ver"' in extract_cmd assert 'ln -sfn "/opt/furtka/versions/$ver" /opt/furtka/current' in extract_cmd +def test_furtka_json_cmd_uses_heredoc_and_interpolates_hostname(install_cmds): + # Regression: the previous base64+sed version of this command was a + # silent no-op on some installs due to archinstall-side quoting; the + # heredoc version is the one that reliably writes furtka.json. + cmd = next((c for c in install_cmds if "/var/lib/furtka/furtka.json" in c), None) + assert cmd is not None + assert "cat > /var/lib/furtka/furtka.json <&2; exit 1; } && ' 'mv "$staging" "/opt/furtka/versions/$ver" && ' + # mktemp -d creates the staging dir with mode 700; that survives the + # mv and leaves Caddy (which runs as the `caddy` user, not root) + # unable to traverse /opt/furtka/current/ when it tries to serve + # the landing page. Open up to 755 so file_server can read. + 'chmod 755 "/opt/furtka/versions/$ver" && ' 'ln -sfn "/opt/furtka/versions/$ver" /opt/furtka/current' ) systemctl_link = "systemctl link " + " ".join( @@ -284,24 +289,23 @@ def _furtka_json_cmd(hostname): at runtime and renders the hostname chip from it. install_date + version ride along so the settings page can display them without hitting the status timer's refresh cycle. + + Heredoc rather than base64 + sed — the previous version had two layers + of quoting that archinstall's custom_commands shell-eval path parsed + inconsistently, leaving this command as a silent no-op on some installs. + The heredoc evaluates `$(date ...)` and `$(cat VERSION)` at chroot + runtime and sidesteps the quoting hazard entirely. Hostname has already + been validated by validate_step1. """ - # Python-side JSON assembly keeps the shell command free of quote-escape - # hazards. Hostname is validated up-front in validate_step1 so it's safe - # to interpolate. - body = json.dumps( - { - "hostname": hostname, - "install_date": "__INSTALL_DATE__", - "version": "__VERSION__", - }, - indent=2, - ) return ( "mkdir -p /var/lib/furtka && " - + _write_file_cmd("/var/lib/furtka/furtka.json", body) - + ' && sed -i "s/__INSTALL_DATE__/$(date -Iseconds)/;' - 's|__VERSION__|$(cat /opt/furtka/current/VERSION 2>/dev/null || echo dev)|"' - " /var/lib/furtka/furtka.json" + "cat > /var/lib/furtka/furtka.json </dev/null || echo dev)"\n' + "}\n" + "EOF" )