Compare commits
No commits in common. "main" and "feat/ui-polish" have entirely different histories.
main
...
feat/ui-po
12 changed files with 16 additions and 180 deletions
27
CHANGELOG.md
27
CHANGELOG.md
|
|
@ -7,19 +7,8 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [26.21-alpha] - 2026-10-23
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **Furtka Gateway: apps reachable from the internet** (behind NAT/CGNAT,
|
|
||||||
no port forwarding). Settings → *Furtka Gateway*: enter the gateway URL
|
|
||||||
and registration token, the box registers and keeps a WireGuard tunnel to
|
|
||||||
a [furtka-gateway](https://forgejo.sourcegate.online/robert/furtka-gateway)
|
|
||||||
server (Traefik + wg-easy + control plane). Apps → ⋯ → *Expose to
|
|
||||||
Internet* publishes an app under its own hostname with Let's Encrypt;
|
|
||||||
*Stop exposing* takes it down again. Only apps whose manifest declares
|
|
||||||
`internet.viable: true` can be exposed. Client in `furtka/gateway.py` +
|
|
||||||
`furtka/wireguard.py`, CLI `furtka gateway …`.
|
|
||||||
- **New logo: Mark M** (a lowercase *f* cut out of an arched gate), winner of
|
- **New logo: Mark M** (a lowercase *f* cut out of an arched gate), winner of
|
||||||
the furtka.org vote (29 of 42 votes). It is now in the website header,
|
the furtka.org vote (29 of 42 votes). It is now in the website header,
|
||||||
favicon and share card, the installer header and tab icon, the on-box UI
|
favicon and share card, the installer header and tab icon, the on-box UI
|
||||||
|
|
@ -73,13 +62,6 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r
|
||||||
full.
|
full.
|
||||||
- **Apps page after an API restart** redirects to `/login` instead of
|
- **Apps page after an API restart** redirects to `/login` instead of
|
||||||
showing an empty app list.
|
showing an empty app list.
|
||||||
- **32-bit UEFI machines** (Atom Bay Trail / Cherry Trail) get Limine
|
|
||||||
instead of systemd-boot: Arch has no ia32 systemd-boot and archinstall's
|
|
||||||
GRUB path hardcodes `x86_64-efi`, so these boxes installed but didn't boot.
|
|
||||||
- **Force-HTTPS redirect** only covers `<hostname>.local` / `<hostname>` and
|
|
||||||
is a 302. The old 301 for every host sent devices that use the IP to a
|
|
||||||
TLS handshake that can't succeed, and stayed cached after HTTPS was
|
|
||||||
turned off. Self-update migrates the old snippet.
|
|
||||||
|
|
||||||
- **Self-update starts newly added timers immediately and links units
|
- **Self-update starts newly added timers immediately and links units
|
||||||
through `current`.** `_link_new_units()` enabled a timer a release
|
through `current`.** `_link_new_units()` enabled a timer a release
|
||||||
|
|
@ -98,12 +80,6 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r
|
||||||
password hint explains it's also the SSH/console password, names are
|
password hint explains it's also the SSH/console password, names are
|
||||||
lowercased instead of rejected, system account names are refused.
|
lowercased instead of rejected, system account names are refused.
|
||||||
- Live console banner says a network cable with internet is required.
|
- Live console banner says a network cable with internet is required.
|
||||||
- ISO builds pin archinstall (4.4-1, sha256-checked from the Arch archive)
|
|
||||||
instead of taking whatever the repos ship that day; the build fails
|
|
||||||
clearly when Arch's Python moves on. Bump steps in `iso/README.md`.
|
|
||||||
- On-box UI polish: sticky top nav, bordered cards and status tiles, app
|
|
||||||
tile hover lift, brand-red section markers; animations respect
|
|
||||||
`prefers-reduced-motion`.
|
|
||||||
|
|
||||||
## [26.20-alpha] - 2026-08-25
|
## [26.20-alpha] - 2026-08-25
|
||||||
|
|
||||||
|
|
@ -659,8 +635,7 @@ First tagged snapshot. Pre-alpha — the installer does not yet boot, but the de
|
||||||
- **Containers:** Docker + Compose
|
- **Containers:** Docker + Compose
|
||||||
- **License:** AGPL-3.0
|
- **License:** AGPL-3.0
|
||||||
|
|
||||||
[Unreleased]: https://forgejo.sourcegate.online/daniel/furtka/compare/26.21-alpha...HEAD
|
[Unreleased]: https://forgejo.sourcegate.online/daniel/furtka/compare/26.20-alpha...HEAD
|
||||||
[26.21-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.21-alpha
|
|
||||||
[26.20-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.20-alpha
|
[26.20-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.20-alpha
|
||||||
[26.19-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.19-alpha
|
[26.19-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.19-alpha
|
||||||
[26.18-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.18-alpha
|
[26.18-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.18-alpha
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ Furtka aims to be:
|
||||||
| Identity provider | Authentik | Bundled SSO from day one — every app template auto-wires to it at install |
|
| Identity provider | Authentik | Bundled SSO from day one — every app template auto-wires to it at install |
|
||||||
| Managed gateway DNS | NS delegation to `ns1.furtka.org` | User delegates once at registrar; we handle wildcard cert + subdomain creation |
|
| Managed gateway DNS | NS delegation to `ns1.furtka.org` | User delegates once at registrar; we handle wildcard cert + subdomain creation |
|
||||||
| Local HTTPS | Local CA | One-click CA install → green padlock on every service, no browser warnings |
|
| Local HTTPS | Local CA | One-click CA install → green padlock on every service, no browser warnings |
|
||||||
| Gateway | Traefik + wg-easy ([furtka-gateway](https://forgejo.sourcegate.online/robert/furtka-gateway)) | Decided 2026-10-23. Self-hosted on any VPS with a public IPv4 via `deploy.sh`, or managed through our infrastructure later. Boxes dial out over WireGuard, so no port forwarding at home. |
|
| Gateway | Flexible | Own reverse proxy OR managed through our infrastructure |
|
||||||
| UI approach | UI-first | Design the simplest possible UI, then build everything to match |
|
| UI approach | UI-first | Design the simplest possible UI, then build everything to match |
|
||||||
|
|
||||||
## Landscape (Existing Projects)
|
## Landscape (Existing Projects)
|
||||||
|
|
@ -122,13 +122,10 @@ None of these nail the "your dad can set this up" experience. The installer wiza
|
||||||
- [x] **Phase 2 Furtka self-update** — `/settings` → Check → Update now. Downloads signed tarball (SHA256), stages, atomic symlink flip, reloads Caddy, daemon-reload, restarts services, health-checks the new api with auto-rollback on failure. CLI: `furtka update [--check]` + `furtka rollback`. Validated end-to-end on VM 2026-04-16 (`26.0-alpha` → `26.3-alpha` → rollback → reboot).
|
- [x] **Phase 2 Furtka self-update** — `/settings` → Check → Update now. Downloads signed tarball (SHA256), stages, atomic symlink flip, reloads Caddy, daemon-reload, restarts services, health-checks the new api with auto-rollback on failure. CLI: `furtka update [--check]` + `furtka rollback`. Validated end-to-end on VM 2026-04-16 (`26.0-alpha` → `26.3-alpha` → rollback → reboot).
|
||||||
- [x] **Local HTTPS Phase 1** — Caddy `tls internal` on `:443` is fully opt-in via the `/settings` toggle (26.15-alpha); fresh installs stay HTTP-only so a half-trusted cert chain can't lock the user out. Per-box root CA generated on first enable, `rootCA.crt` downloadable from `/settings`, per-OS install guide at `/https-install/`. The "force HTTPS" sub-toggle still only appears once the current browser already trusts the cert.
|
- [x] **Local HTTPS Phase 1** — Caddy `tls internal` on `:443` is fully opt-in via the `/settings` toggle (26.15-alpha); fresh installs stay HTTP-only so a half-trusted cert chain can't lock the user out. Per-box root CA generated on first enable, `rootCA.crt` downloadable from `/settings`, per-OS install guide at `/https-install/`. The "force HTTPS" sub-toggle still only appears once the current browser already trusts the cert.
|
||||||
- [x] **Post-build smoke VM on Proxmox** — `.forgejo/workflows/build-iso.yml` hands the freshly built ISO to `scripts/smoke-vm.sh`, which boots it in a throwaway VM on `pollux` (192.168.178.165) and curls the webinstaller on `:5000`. VMID range 9000–9099, last 5 kept. Green end-to-end since 26.4-alpha.
|
- [x] **Post-build smoke VM on Proxmox** — `.forgejo/workflows/build-iso.yml` hands the freshly built ISO to `scripts/smoke-vm.sh`, which boots it in a throwaway VM on `pollux` (192.168.178.165) and curls the webinstaller on `:5000`. VMID range 9000–9099, last 5 kept. Green end-to-end since 26.4-alpha.
|
||||||
- [x] **Furtka Gateway (Access Gate)** — boxes behind NAT/CGNAT register with a [furtka-gateway](https://forgejo.sourcegate.online/robert/furtka-gateway) server (Traefik + wg-easy + control plane) over a WireGuard tunnel; Settings → *Furtka Gateway* connects, Apps → ⋯ → *Expose to Internet* publishes an app under its own hostname with Let's Encrypt. Only apps with `internet.viable: true` in the manifest can be exposed. Shipped in `26.21-alpha`; first gateway running at `gw.furtka.org` (Hetzner, 2026-10-23).
|
|
||||||
- [x] **`26.21-alpha`** — security fixes (app-name validation, `.env` injection, fail-closed `users.json`, API hardening), installer fixes, Mark M logo, furtka.org/download, on-box UI polish. See [CHANGELOG](CHANGELOG.md).
|
|
||||||
- [ ] Gateway dogfooding on real hardware — two old laptops as boxes, one per brother, both behind CGNAT
|
|
||||||
- [ ] Installer wizard screens S3–S7 — per-device purpose, network, domain, SSL, diagnostic. S5/S6 blocked on managed-gateway DNS infra not yet built.
|
- [ ] Installer wizard screens S3–S7 — per-device purpose, network, domain, SSL, diagnostic. S5/S6 blocked on managed-gateway DNS infra not yet built.
|
||||||
- [ ] Local HTTPS Phase 2 — dedicated local CA (not Caddy's `tls internal`), streamlined one-click install across Win/Mac/Linux/Android, and HTTPS on the live-installer wizard (`https://proksi.local:5000`).
|
- [ ] Local HTTPS Phase 2 — dedicated local CA (not Caddy's `tls internal`), streamlined one-click install across Win/Mac/Linux/Android, and HTTPS on the live-installer wizard (`https://proksi.local:5000`).
|
||||||
- [ ] Caddy + Authentik wired into first-boot bootstrap
|
- [ ] Caddy + Authentik wired into first-boot bootstrap
|
||||||
- [ ] Managed gateway DNS — `ns1/ns2.furtka.org` + DNS-01 wildcard automation (the gateway itself works today with your own domain and per-app or wildcard A records)
|
- [ ] Managed gateway infrastructure — `ns1/ns2.furtka.org` + DNS-01 wildcard automation
|
||||||
- [ ] First containerized service (Nextcloud?) with auto-SSO + auto-subdomain
|
- [ ] First containerized service (Nextcloud?) with auto-SSO + auto-subdomain
|
||||||
- [ ] Competitor hands-on testing on Proxmox — validate findings from docs/competitors.md
|
- [ ] Competitor hands-on testing on Proxmox — validate findings from docs/competitors.md
|
||||||
- [ ] UI mockups / drafts (Robert)
|
- [ ] UI mockups / drafts (Robert)
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,7 @@ from pathlib import Path
|
||||||
CA_CERT_PATH = Path("/var/lib/caddy/pki/authorities/local/root.crt")
|
CA_CERT_PATH = Path("/var/lib/caddy/pki/authorities/local/root.crt")
|
||||||
SNIPPET_DIR = Path("/etc/caddy/furtka.d")
|
SNIPPET_DIR = Path("/etc/caddy/furtka.d")
|
||||||
REDIRECT_SNIPPET = SNIPPET_DIR / "redirect.caddyfile"
|
REDIRECT_SNIPPET = SNIPPET_DIR / "redirect.caddyfile"
|
||||||
# Pre-26.21 redirect: permanent (browsers cache a 301 indefinitely, so
|
REDIRECT_CONTENT = "redir https://{host}{uri} permanent\n"
|
||||||
# turning HTTPS off left them bouncing to a dead :443) and for every Host —
|
|
||||||
# including a bare IP, which has no certificate, so devices that reach the
|
|
||||||
# box by IP (no mDNS on Android / older Windows) couldn't load the UI at all.
|
|
||||||
LEGACY_REDIRECT_CONTENT = "redir https://{host}{uri} permanent\n"
|
|
||||||
HTTPS_SNIPPET_DIR = Path("/etc/caddy/furtka-https.d")
|
HTTPS_SNIPPET_DIR = Path("/etc/caddy/furtka-https.d")
|
||||||
HTTPS_SNIPPET = HTTPS_SNIPPET_DIR / "https.caddyfile"
|
HTTPS_SNIPPET = HTTPS_SNIPPET_DIR / "https.caddyfile"
|
||||||
HOSTNAME_FILE = Path("/etc/hostname")
|
HOSTNAME_FILE = Path("/etc/hostname")
|
||||||
|
|
@ -66,19 +62,6 @@ def _read_hostname(hostname_file: Path = HOSTNAME_FILE) -> str:
|
||||||
return value or "furtka"
|
return value or "furtka"
|
||||||
|
|
||||||
|
|
||||||
def _redirect_snippet_content(hostname: str) -> str:
|
|
||||||
"""HTTP→HTTPS redirect for the names the HTTPS block has a cert for.
|
|
||||||
|
|
||||||
Only those hosts are redirected: an IP or any other name keeps plain
|
|
||||||
HTTP instead of landing on a TLS handshake that can't succeed. 302 so
|
|
||||||
switching HTTPS off takes effect in browsers immediately.
|
|
||||||
"""
|
|
||||||
return (
|
|
||||||
f"@furtka_https_host host {hostname}.local {hostname}\n"
|
|
||||||
"redir @furtka_https_host https://{host}{uri} 302\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _https_snippet_content(hostname: str) -> str:
|
def _https_snippet_content(hostname: str) -> str:
|
||||||
"""Caddy site block the HTTPS toggle installs at opt-in.
|
"""Caddy site block the HTTPS toggle installs at opt-in.
|
||||||
|
|
||||||
|
|
@ -168,9 +151,8 @@ def set_force_https(
|
||||||
previous_https = https_snippet.read_text() if had_https else None
|
previous_https = https_snippet.read_text() if had_https else None
|
||||||
|
|
||||||
if enabled:
|
if enabled:
|
||||||
hostname = _read_hostname(hostname_file)
|
snippet.write_text(REDIRECT_CONTENT)
|
||||||
snippet.write_text(_redirect_snippet_content(hostname))
|
https_snippet.write_text(_https_snippet_content(_read_hostname(hostname_file)))
|
||||||
https_snippet.write_text(_https_snippet_content(hostname))
|
|
||||||
else:
|
else:
|
||||||
if had_redirect:
|
if had_redirect:
|
||||||
snippet.unlink()
|
snippet.unlink()
|
||||||
|
|
|
||||||
|
|
@ -192,20 +192,6 @@ def _maybe_migrate_preserve_https() -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _maybe_migrate_redirect_snippet() -> None:
|
|
||||||
"""26.21 migration: swap the old catch-all 301 redirect for the
|
|
||||||
hostname-scoped 302 one, so boxes reached by IP can load the UI again
|
|
||||||
and turning HTTPS off stops sticking in browser caches."""
|
|
||||||
from furtka import https
|
|
||||||
|
|
||||||
redirect_snippet = _CADDY_SNIPPET_DIR / "redirect.caddyfile"
|
|
||||||
if not redirect_snippet.is_file():
|
|
||||||
return
|
|
||||||
if redirect_snippet.read_text() != https.LEGACY_REDIRECT_CONTENT:
|
|
||||||
return
|
|
||||||
redirect_snippet.write_text(https._redirect_snippet_content(_current_hostname()))
|
|
||||||
|
|
||||||
|
|
||||||
def _refresh_caddyfile(source: Path) -> bool:
|
def _refresh_caddyfile(source: Path) -> bool:
|
||||||
"""Copy the shipped Caddyfile to /etc/caddy/ iff it differs. Returns True
|
"""Copy the shipped Caddyfile to /etc/caddy/ iff it differs. Returns True
|
||||||
if the file changed (so caddy needs more than a bare reload).
|
if the file changed (so caddy needs more than a bare reload).
|
||||||
|
|
@ -229,7 +215,6 @@ def _refresh_caddyfile(source: Path) -> bool:
|
||||||
# hostname block is gone, so the redirect would 301 to a dead :443.
|
# hostname block is gone, so the redirect would 301 to a dead :443.
|
||||||
# Preserve intent by writing the HTTPS listener snippet too.
|
# Preserve intent by writing the HTTPS listener snippet too.
|
||||||
_maybe_migrate_preserve_https()
|
_maybe_migrate_preserve_https()
|
||||||
_maybe_migrate_redirect_snippet()
|
|
||||||
rendered = source.read_text().replace(_CADDYFILE_HOSTNAME_MARKER, _current_hostname())
|
rendered = source.read_text().replace(_CADDYFILE_HOSTNAME_MARKER, _current_hostname())
|
||||||
if _CADDYFILE_LIVE.is_file() and rendered == _CADDYFILE_LIVE.read_text():
|
if _CADDYFILE_LIVE.is_file() and rendered == _CADDYFILE_LIVE.read_text():
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
|
|
@ -34,17 +34,6 @@ The systemd service runs `flask --app app run --host 0.0.0.0 --port 5000` under
|
||||||
|
|
||||||
mDNS is wired: `avahi-daemon` + `nss-mdns` come from `packages.extra`, the live ISO's hostname is `proksi`, and as soon as `systemd-networkd-wait-online` fires the installer is reachable at `http://proksi.local:5000`. The raw IP still shows on the console for fallback — some Windows clients need the Bonjour service for `.local` to resolve at all.
|
mDNS is wired: `avahi-daemon` + `nss-mdns` come from `packages.extra`, the live ISO's hostname is `proksi`, and as soon as `systemd-networkd-wait-online` fires the installer is reachable at `http://proksi.local:5000`. The raw IP still shows on the console for fallback — some Windows clients need the Bonjour service for `.local` to resolve at all.
|
||||||
|
|
||||||
### archinstall is pinned
|
|
||||||
|
|
||||||
The webinstaller imports archinstall internals and `furtka-webinstaller.service` sed-patches one of its files, so `build.sh` does not take whatever archinstall the Arch repos currently ship. It downloads the version in `ARCHINSTALL_PIN` from `archive.archlinux.org`, checks it against `ARCHINSTALL_SHA256`, and serves it from a local `[furtka-pinned]` repo listed before `[core]` in the build's `pacman.conf` (pacman takes a package from the first repo that has it). Only the build uses that repo; the live system's own `pacman.conf` is untouched.
|
|
||||||
|
|
||||||
The build fails with "pinned archinstall … targets Python X, Arch now ships Y" once Arch moves to a new Python — the pinned package would install into a dead `site-packages`. To bump:
|
|
||||||
|
|
||||||
1. Pick the new version from <https://archive.archlinux.org/packages/a/archinstall/>.
|
|
||||||
2. `curl -LO` the `.pkg.tar.zst`, `sha256sum` it, and update `ARCHINSTALL_PIN` + `ARCHINSTALL_SHA256` at the top of `build.sh`.
|
|
||||||
3. Check that `webinstaller/app.py:build_disk_config` still imports and that the `ExecStartPre` sed in `furtka-webinstaller.service` still matches (`journalctl -u furtka-webinstaller` on the live ISO).
|
|
||||||
4. Full VM install (UEFI and BIOS) before merging.
|
|
||||||
|
|
||||||
`build.sh` also deletes releng's `getty@tty1.service.d/autologin.conf`. With root auto-logged-in, tty1 belongs to a shell and `agetty --reload` can't redraw the banner, so the IP line was a snapshot of whatever DHCP had done by the time getty started. Without autologin tty1 sits at the login prompt showing the live banner; for a shell type `root` + Enter (releng's root has no password) — the banner says so.
|
`build.sh` also deletes releng's `getty@tty1.service.d/autologin.conf`. With root auto-logged-in, tty1 belongs to a shell and `agetty --reload` can't redraw the banner, so the IP line was a snapshot of whatever DHCP had done by the time getty started. Without autologin tty1 sits at the login prompt showing the live banner; for a shell type `root` + Enter (releng's root has no password) — the banner says so.
|
||||||
|
|
||||||
## Test flow
|
## Test flow
|
||||||
|
|
|
||||||
37
iso/build.sh
37
iso/build.sh
|
|
@ -27,11 +27,7 @@ fi
|
||||||
# ---- inside the container from here on ----
|
# ---- inside the container from here on ----
|
||||||
|
|
||||||
echo "==> Syncing pacman, installing archiso"
|
echo "==> Syncing pacman, installing archiso"
|
||||||
pacman -Syu --noconfirm --needed archiso curl zstd
|
pacman -Syu --noconfirm --needed archiso
|
||||||
|
|
||||||
# archinstall build the webinstaller is validated against (see pin step below).
|
|
||||||
ARCHINSTALL_PIN="4.4-1"
|
|
||||||
ARCHINSTALL_SHA256="6cb5da75c108ef2f5d25a7a20baf6c52cc927d5e06890dd10182e6eb4be815cb"
|
|
||||||
|
|
||||||
PROFILE_SRC="/usr/share/archiso/configs/releng"
|
PROFILE_SRC="/usr/share/archiso/configs/releng"
|
||||||
PROFILE_WORK="/tmp/furtka-profile"
|
PROFILE_WORK="/tmp/furtka-profile"
|
||||||
|
|
@ -47,37 +43,6 @@ cat "$SCRIPT_DIR/overlay/packages.extra" >> "$PROFILE_WORK/packages.x86_64"
|
||||||
|
|
||||||
cat "$SCRIPT_DIR/overlay/profiledef.sh" >> "$PROFILE_WORK/profiledef.sh"
|
cat "$SCRIPT_DIR/overlay/profiledef.sh" >> "$PROFILE_WORK/profiledef.sh"
|
||||||
|
|
||||||
echo "==> Pinning archinstall ${ARCHINSTALL_PIN}"
|
|
||||||
# The webinstaller imports archinstall internals (build_disk_config) and
|
|
||||||
# sed-patches one of its files at service start, so a silent archinstall
|
|
||||||
# upgrade in the Arch repos can break every install with no change on our
|
|
||||||
# side. Serve the validated build from a local repo listed *before* [core]:
|
|
||||||
# pacman takes a package from the first repo that has it, whatever the
|
|
||||||
# version elsewhere. Bump deliberately: new version + sha256 below, rebuild,
|
|
||||||
# run the smoke VM (see iso/README.md).
|
|
||||||
PINNED_REPO="/tmp/furtka-pinned-repo"
|
|
||||||
rm -rf "$PINNED_REPO" && mkdir -p "$PINNED_REPO"
|
|
||||||
ARCHINSTALL_PKG="archinstall-${ARCHINSTALL_PIN}-any.pkg.tar.zst"
|
|
||||||
curl -fsSL --retry 3 -o "$PINNED_REPO/$ARCHINSTALL_PKG" \
|
|
||||||
"https://archive.archlinux.org/packages/a/archinstall/$ARCHINSTALL_PKG"
|
|
||||||
echo "${ARCHINSTALL_SHA256} $PINNED_REPO/$ARCHINSTALL_PKG" | sha256sum -c -
|
|
||||||
# archinstall is an "any" package installed under /usr/lib/python3.X; after
|
|
||||||
# Arch moves to a new Python it would install into a dead directory. Fail
|
|
||||||
# the build with a clear message instead of shipping a broken installer.
|
|
||||||
pkg_py=$(tar --zstd -tf "$PINNED_REPO/$ARCHINSTALL_PKG" \
|
|
||||||
| sed -n 's|^usr/lib/python\(3\.[0-9]*\)/.*|\1|p' | sort -u)
|
|
||||||
repo_py=$(pacman -Si python | awk -F': ' '/^Version/ {split($2, v, "."); print v[1] "." v[2]}')
|
|
||||||
if [ "$pkg_py" != "$repo_py" ]; then
|
|
||||||
echo "ERROR: pinned archinstall ${ARCHINSTALL_PIN} targets Python $pkg_py, Arch now ships $repo_py." >&2
|
|
||||||
echo " Bump ARCHINSTALL_PIN/ARCHINSTALL_SHA256 in iso/build.sh (see iso/README.md)." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
repo-add -q "$PINNED_REPO/furtka-pinned.db.tar.gz" "$PINNED_REPO/$ARCHINSTALL_PKG"
|
|
||||||
# The sha256 above already pins the exact bytes.
|
|
||||||
sed -i "0,/^\[core\]/s||[furtka-pinned]\nSigLevel = Optional TrustAll\nServer = file://$PINNED_REPO\n\n[core]|" \
|
|
||||||
"$PROFILE_WORK/pacman.conf"
|
|
||||||
grep -q '^\[furtka-pinned\]' "$PROFILE_WORK/pacman.conf"
|
|
||||||
|
|
||||||
cp -a "$SCRIPT_DIR/overlay/airootfs/." "$PROFILE_WORK/airootfs/"
|
cp -a "$SCRIPT_DIR/overlay/airootfs/." "$PROFILE_WORK/airootfs/"
|
||||||
|
|
||||||
# releng auto-logs root in on tty1. That turns our /etc/issue banner into a
|
# releng auto-logs root in on tty1. That turns our /etc/issue banner into a
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "furtka"
|
name = "furtka"
|
||||||
version = "26.21-alpha"
|
version = "26.20-alpha"
|
||||||
description = "Open-source home server OS — simple enough for everyone."
|
description = "Open-source home server OS — simple enough for everyone."
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
||||||
|
|
@ -390,16 +390,3 @@ def test_error_page_offers_retry(monkeypatch, tmp_path):
|
||||||
html = app_module.app.test_client().get("/install/log").get_data(as_text=True)
|
html = app_module.app.test_client().get("/install/log").get_data(as_text=True)
|
||||||
assert "Try again" in html
|
assert "Try again" in html
|
||||||
assert "next-steps" not in html
|
assert "next-steps" not in html
|
||||||
|
|
||||||
|
|
||||||
def test_detect_bootloader_by_firmware(tmp_path):
|
|
||||||
import app as app_module
|
|
||||||
|
|
||||||
efi = tmp_path / "efi"
|
|
||||||
assert app_module._detect_bootloader(efi) == "Grub" # BIOS / legacy
|
|
||||||
efi.mkdir()
|
|
||||||
assert app_module._detect_bootloader(efi) == "Systemd-boot" # no bitness file
|
|
||||||
(efi / "fw_platform_size").write_text("64\n")
|
|
||||||
assert app_module._detect_bootloader(efi) == "Systemd-boot"
|
|
||||||
(efi / "fw_platform_size").write_text("32\n")
|
|
||||||
assert app_module._detect_bootloader(efi) == "Limine"
|
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ def test_set_force_enable_writes_both_snippets_and_reloads(tmp_path):
|
||||||
|
|
||||||
result = https.set_force_https(True, reload_caddy=fake_reload, **p)
|
result = https.set_force_https(True, reload_caddy=fake_reload, **p)
|
||||||
assert result is True
|
assert result is True
|
||||||
assert p["snippet"].read_text() == https._redirect_snippet_content("testbox")
|
assert p["snippet"].read_text() == https.REDIRECT_CONTENT
|
||||||
written = p["https_snippet"].read_text()
|
written = p["https_snippet"].read_text()
|
||||||
assert "testbox.local, testbox" in written
|
assert "testbox.local, testbox" in written
|
||||||
assert "tls internal" in written
|
assert "tls internal" in written
|
||||||
|
|
@ -135,7 +135,7 @@ def test_set_force_disable_removes_both_snippets(tmp_path):
|
||||||
p = _paths(tmp_path)
|
p = _paths(tmp_path)
|
||||||
p["snippet_dir"].mkdir()
|
p["snippet_dir"].mkdir()
|
||||||
p["https_snippet_dir"].mkdir()
|
p["https_snippet_dir"].mkdir()
|
||||||
p["snippet"].write_text(https._redirect_snippet_content("testbox"))
|
p["snippet"].write_text(https.REDIRECT_CONTENT)
|
||||||
p["https_snippet"].write_text("furtka.local { tls internal }\n")
|
p["https_snippet"].write_text("furtka.local { tls internal }\n")
|
||||||
|
|
||||||
result = https.set_force_https(False, reload_caddy=lambda: None, **p)
|
result = https.set_force_https(False, reload_caddy=lambda: None, **p)
|
||||||
|
|
@ -199,16 +199,10 @@ def test_systemctl_missing_raises_and_rolls_back(tmp_path):
|
||||||
assert not p["https_snippet"].exists()
|
assert not p["https_snippet"].exists()
|
||||||
|
|
||||||
|
|
||||||
def test_redirect_snippet_only_covers_cert_names_and_is_temporary():
|
def test_redirect_snippet_content_is_caddy_redir_directive():
|
||||||
# Lock the exact directives. A regression here silently stops the
|
# Lock the exact directive. A regression here silently stops the
|
||||||
# redirect from taking effect even though the file-swap looks fine.
|
# redirect from taking effect even though the file-swap looks fine.
|
||||||
# Only the names the tls-internal block has a cert for are redirected
|
assert https.REDIRECT_CONTENT.strip() == "redir https://{host}{uri} permanent"
|
||||||
# (an IP would hit a failing TLS handshake), and 302 so switching HTTPS
|
|
||||||
# off isn't stuck in browser caches like the old 301 was.
|
|
||||||
assert https._redirect_snippet_content("mybox") == (
|
|
||||||
"@furtka_https_host host mybox.local mybox\n"
|
|
||||||
"redir @furtka_https_host https://{host}{uri} 302\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_https_snippet_content_has_tls_internal_and_routes(tmp_path):
|
def test_https_snippet_content_has_tls_internal_and_routes(tmp_path):
|
||||||
|
|
|
||||||
|
|
@ -539,31 +539,3 @@ def test_health_check_survives_read_timeout(updater, monkeypatch):
|
||||||
monkeypatch.setattr("urllib.request.urlopen", slow)
|
monkeypatch.setattr("urllib.request.urlopen", slow)
|
||||||
monkeypatch.setattr(updater.time, "sleep", lambda _s: None)
|
monkeypatch.setattr(updater.time, "sleep", lambda _s: None)
|
||||||
assert updater._health_check("http://127.0.0.1:7000/api/apps", deadline_s=0.05) is False
|
assert updater._health_check("http://127.0.0.1:7000/api/apps", deadline_s=0.05) is False
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_caddyfile_migrates_legacy_301_redirect(updater, tmp_path):
|
|
||||||
from furtka import https
|
|
||||||
|
|
||||||
snippet_dir = tmp_path / "etc_caddy" / "furtka.d"
|
|
||||||
snippet_dir.mkdir(parents=True)
|
|
||||||
(tmp_path / "etc_caddy" / "furtka-https.d").mkdir()
|
|
||||||
(tmp_path / "etc_caddy" / "furtka-https.d" / "https.caddyfile").write_text("x\n")
|
|
||||||
redirect = snippet_dir / "redirect.caddyfile"
|
|
||||||
redirect.write_text(https.LEGACY_REDIRECT_CONTENT)
|
|
||||||
src = tmp_path / "Caddyfile.src"
|
|
||||||
src.write_text("# caddy\n")
|
|
||||||
updater._refresh_caddyfile(src)
|
|
||||||
assert redirect.read_text() == https._redirect_snippet_content("testbox")
|
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_caddyfile_leaves_custom_redirect_alone(updater, tmp_path):
|
|
||||||
snippet_dir = tmp_path / "etc_caddy" / "furtka.d"
|
|
||||||
snippet_dir.mkdir(parents=True)
|
|
||||||
(tmp_path / "etc_caddy" / "furtka-https.d").mkdir()
|
|
||||||
(tmp_path / "etc_caddy" / "furtka-https.d" / "https.caddyfile").write_text("x\n")
|
|
||||||
redirect = snippet_dir / "redirect.caddyfile"
|
|
||||||
redirect.write_text("# hand-edited\n")
|
|
||||||
src = tmp_path / "Caddyfile.src"
|
|
||||||
src.write_text("# caddy\n")
|
|
||||||
updater._refresh_caddyfile(src)
|
|
||||||
assert redirect.read_text() == "# hand-edited\n"
|
|
||||||
|
|
|
||||||
|
|
@ -515,20 +515,10 @@ def _post_install_commands(hostname, admin_username, admin_password):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _detect_bootloader(efi_dir=Path("/sys/firmware/efi")):
|
def _detect_bootloader():
|
||||||
# systemd-boot is UEFI-only; on BIOS/legacy it trips HardwareIncompatibilityError
|
# systemd-boot is UEFI-only; on BIOS/legacy it trips HardwareIncompatibilityError
|
||||||
# inside archinstall. /sys/firmware/efi exists iff we were booted via UEFI.
|
# inside archinstall. /sys/firmware/efi exists iff we were booted via UEFI.
|
||||||
if not efi_dir.exists():
|
return "Systemd-boot" if Path("/sys/firmware/efi").exists() else "Grub"
|
||||||
return "Grub"
|
|
||||||
# 64-bit CPU behind 32-bit UEFI (Atom Bay Trail / Cherry Trail mini-PCs
|
|
||||||
# and tablets): Arch ships no ia32 systemd-boot, and archinstall's GRUB
|
|
||||||
# path hardcodes --target=x86_64-efi. Its Limine path installs both
|
|
||||||
# BOOTIA32.EFI and BOOTX64.EFI and picks the entry by firmware bitness.
|
|
||||||
try:
|
|
||||||
bitness = (efi_dir / "fw_platform_size").read_text().strip()
|
|
||||||
except OSError:
|
|
||||||
bitness = "64"
|
|
||||||
return "Limine" if bitness == "32" else "Systemd-boot"
|
|
||||||
|
|
||||||
|
|
||||||
def build_archinstall_config(s):
|
def build_archinstall_config(s):
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ enableRobotsTXT = true
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
description = "Open-source home server OS — simple enough for everyone."
|
description = "Open-source home server OS — simple enough for everyone."
|
||||||
version = "26.21-alpha"
|
version = "26.20-alpha"
|
||||||
contactEmail = "hallo@furtka.org"
|
contactEmail = "hallo@furtka.org"
|
||||||
# Forgejo repo the download page reads its latest release (ISO asset,
|
# Forgejo repo the download page reads its latest release (ISO asset,
|
||||||
# size, date, checksum) from at build time. `version` above is only the
|
# size, date, checksum) from at build time. `version` above is only the
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue