furtka/furtka/installer.py

320 lines
11 KiB
Python
Raw Normal View History

feat(furtka): reconciler + install/remove — slice 2 Fills in the act-on-it half of the resource manager. Reconciler walks the scanner output and brings docker into the desired state: ensures each manifest-declared volume exists (idempotent), then runs docker compose up -d for the project. install/remove on the CLI work end-to-end against a real /var/lib/furtka/apps/ tree. - furtka.dockerops: thin subprocess wrapper. Volume + compose primitives that other modules call. `_run` raises DockerError with the actual stderr so failures are diagnosable. - furtka.reconciler: builds an ordered Action list (volumes then compose_up per app), executes unless dry-run. Broken manifests produce a "skip" action, the rest of the apps still get reconciled. - furtka.installer: copy-from-source with two non-obvious rules — user .env is preserved across upgrade installs, and a missing .env is bootstrapped from .env.example so compose has values to substitute on first install. Bundled-app lookup falls back to /opt/furtka/apps/<name>/ when the source arg isn't a path. - furtka.cli: app install/remove wired up. remove() ignores compose down failures so a botched compose doesn't trap users with an un-removable folder. - 15 new tests using monkeypatch'd dockerops so the suite still runs without docker installed. Covers reconcile dry-run, multi-volume apps, broken-manifest skip behavior, .env preservation, bundled-name resolution, and remove edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:02:00 +02:00
import shutil
from pathlib import Path
feat(catalog): on-box apps catalog synced independently of core version New `furtka catalog sync` pulls the latest daniel/furtka-apps release, verifies its sha256, extracts under /var/lib/furtka/catalog/, and atomically swaps into place — so apps can ship without cutting a new Furtka core release. A daily timer (furtka-catalog-sync.timer, 10 min post-boot + 24 h with ±6 h jitter) drives the sync; /apps gets a manual "Sync apps catalog" button that kicks the same code path via a detached systemd-run unit. Layout of the new on-box tree: /var/lib/furtka/catalog/ synced catalog (survives self-updates) ├── VERSION └── apps/<name>/ ... /var/lib/furtka/catalog-state.json sync stage + last version, UI-polled /run/furtka/catalog.lock flock so timer + manual click can't race Resolver precedence (furtka/sources.py): catalog wins over the bundled seed (/opt/furtka/current/apps/, carried by the core release for offline first-boot). Installed apps under /var/lib/furtka/apps/ are never auto- swapped — user clicks Reinstall to move an existing install onto a newer catalog version; settings merge-preserved via the existing installer.install_from path. New files: - furtka/_release_common.py — shared Forgejo/tarball primitives lifted from furtka/updater.py. Both modules now import from here; updater's behaviour and public API unchanged. - furtka/catalog.py — check_catalog(), sync_catalog() with staging + manifest validation + atomic rename. Refuses bad sha256 / broken manifests and leaves the live catalog intact on any failure path. - furtka/sources.py — resolve_app_name() / list_available() abstraction used by installer.resolve_source and api._list_available. - assets/systemd/furtka-catalog-sync.{service,timer} — oneshot service + daily timer. Timer auto-enables on self-update via a one-line addition to _link_new_units (fresh installs get enabled via the webinstaller's _FURTKA_UNITS list). API + UI: - /api/bundled renamed internally to _list_available; endpoint stays as a backcompat alias; /api/apps/available is the new canonical name. Each list entry carries a `source` field ("catalog" | "bundled"). - POST /api/catalog/sync/check + /apply + GET /api/catalog/status. - /apps page grows a catalog-status row + Sync button; poll loop mirrors the Furtka self-update flow. CLI: `furtka catalog sync [--check]` + `furtka catalog status` (both support --json). Old `furtka app install` / `reconcile` / `update` / `rollback` surfaces are unchanged. Test gate: 194/170 baseline + 24 new tests covering catalog sync (happy path, sha256 mismatch, invalid manifest, lock contention, preserves-on-failure) + resolver precedence + api renames. ruff check + format clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:16:02 +02:00
from furtka import sources
from furtka.manifest import Manifest, ManifestError, load_manifest
feat(catalog): on-box apps catalog synced independently of core version New `furtka catalog sync` pulls the latest daniel/furtka-apps release, verifies its sha256, extracts under /var/lib/furtka/catalog/, and atomically swaps into place — so apps can ship without cutting a new Furtka core release. A daily timer (furtka-catalog-sync.timer, 10 min post-boot + 24 h with ±6 h jitter) drives the sync; /apps gets a manual "Sync apps catalog" button that kicks the same code path via a detached systemd-run unit. Layout of the new on-box tree: /var/lib/furtka/catalog/ synced catalog (survives self-updates) ├── VERSION └── apps/<name>/ ... /var/lib/furtka/catalog-state.json sync stage + last version, UI-polled /run/furtka/catalog.lock flock so timer + manual click can't race Resolver precedence (furtka/sources.py): catalog wins over the bundled seed (/opt/furtka/current/apps/, carried by the core release for offline first-boot). Installed apps under /var/lib/furtka/apps/ are never auto- swapped — user clicks Reinstall to move an existing install onto a newer catalog version; settings merge-preserved via the existing installer.install_from path. New files: - furtka/_release_common.py — shared Forgejo/tarball primitives lifted from furtka/updater.py. Both modules now import from here; updater's behaviour and public API unchanged. - furtka/catalog.py — check_catalog(), sync_catalog() with staging + manifest validation + atomic rename. Refuses bad sha256 / broken manifests and leaves the live catalog intact on any failure path. - furtka/sources.py — resolve_app_name() / list_available() abstraction used by installer.resolve_source and api._list_available. - assets/systemd/furtka-catalog-sync.{service,timer} — oneshot service + daily timer. Timer auto-enables on self-update via a one-line addition to _link_new_units (fresh installs get enabled via the webinstaller's _FURTKA_UNITS list). API + UI: - /api/bundled renamed internally to _list_available; endpoint stays as a backcompat alias; /api/apps/available is the new canonical name. Each list entry carries a `source` field ("catalog" | "bundled"). - POST /api/catalog/sync/check + /apply + GET /api/catalog/status. - /apps page grows a catalog-status row + Sync button; poll loop mirrors the Furtka self-update flow. CLI: `furtka catalog sync [--check]` + `furtka catalog status` (both support --json). Old `furtka app install` / `reconcile` / `update` / `rollback` surfaces are unchanged. Test gate: 194/170 baseline + 24 new tests covering catalog sync (happy path, sha256 mismatch, invalid manifest, lock contention, preserves-on-failure) + resolver precedence + api renames. ruff check + format clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:16:02 +02:00
from furtka.paths import apps_dir
feat(furtka): reconciler + install/remove — slice 2 Fills in the act-on-it half of the resource manager. Reconciler walks the scanner output and brings docker into the desired state: ensures each manifest-declared volume exists (idempotent), then runs docker compose up -d for the project. install/remove on the CLI work end-to-end against a real /var/lib/furtka/apps/ tree. - furtka.dockerops: thin subprocess wrapper. Volume + compose primitives that other modules call. `_run` raises DockerError with the actual stderr so failures are diagnosable. - furtka.reconciler: builds an ordered Action list (volumes then compose_up per app), executes unless dry-run. Broken manifests produce a "skip" action, the rest of the apps still get reconciled. - furtka.installer: copy-from-source with two non-obvious rules — user .env is preserved across upgrade installs, and a missing .env is bootstrapped from .env.example so compose has values to substitute on first install. Bundled-app lookup falls back to /opt/furtka/apps/<name>/ when the source arg isn't a path. - furtka.cli: app install/remove wired up. remove() ignores compose down failures so a botched compose doesn't trap users with an un-removable folder. - 15 new tests using monkeypatch'd dockerops so the suite still runs without docker installed. Covers reconcile dry-run, multi-volume apps, broken-manifest skip behavior, .env preservation, bundled-name resolution, and remove edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:02:00 +02:00
2026-04-15 10:17:00 +02:00
# Values that an app's .env.example may use as obvious "fill me in" markers.
# If any of these reach the live .env, install refuses — otherwise we'd ship
# an SMB share with password "changeme" out of the box, which is the kind of
# default that ends up screenshotted on Hacker News.
PLACEHOLDER_SECRETS: frozenset[str] = frozenset({"changeme"})
# System paths that must never be accepted as a user-supplied `path`-type
# setting. The user is root on their own box, so this is about preventing
# accidental footguns (typing `/etc` when they meant `/mnt/etc`), not
# defending against an attacker. Matches exact paths and their subtrees
# after `Path.resolve()` — so `/mnt/../etc` also lands here.
DENIED_PATH_PREFIXES: tuple[str, ...] = (
"/etc",
"/root",
"/boot",
"/proc",
"/sys",
"/dev",
"/bin",
"/sbin",
"/usr/bin",
"/usr/sbin",
"/var/lib/furtka",
)
feat(furtka): reconciler + install/remove — slice 2 Fills in the act-on-it half of the resource manager. Reconciler walks the scanner output and brings docker into the desired state: ensures each manifest-declared volume exists (idempotent), then runs docker compose up -d for the project. install/remove on the CLI work end-to-end against a real /var/lib/furtka/apps/ tree. - furtka.dockerops: thin subprocess wrapper. Volume + compose primitives that other modules call. `_run` raises DockerError with the actual stderr so failures are diagnosable. - furtka.reconciler: builds an ordered Action list (volumes then compose_up per app), executes unless dry-run. Broken manifests produce a "skip" action, the rest of the apps still get reconciled. - furtka.installer: copy-from-source with two non-obvious rules — user .env is preserved across upgrade installs, and a missing .env is bootstrapped from .env.example so compose has values to substitute on first install. Bundled-app lookup falls back to /opt/furtka/apps/<name>/ when the source arg isn't a path. - furtka.cli: app install/remove wired up. remove() ignores compose down failures so a botched compose doesn't trap users with an un-removable folder. - 15 new tests using monkeypatch'd dockerops so the suite still runs without docker installed. Covers reconcile dry-run, multi-volume apps, broken-manifest skip behavior, .env preservation, bundled-name resolution, and remove edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:02:00 +02:00
class InstallError(RuntimeError):
pass
2026-04-15 10:17:00 +02:00
def _placeholder_keys(env_path: Path) -> list[str]:
if not env_path.exists():
return []
bad: list[str] = []
for raw in env_path.read_text().splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
value = value.strip().strip('"').strip("'")
if value in PLACEHOLDER_SECRETS:
bad.append(key.strip())
return bad
def _is_denied_system_path(resolved: str) -> bool:
if resolved == "/":
return True
for bad in DENIED_PATH_PREFIXES:
if resolved == bad or resolved.startswith(bad + "/"):
return True
return False
def _path_setting_errors(m: Manifest, env_path: Path) -> list[str]:
"""Validate the filesystem paths named by `path`-type settings.
Returns one human-readable message per offending setting. Empty values
on non-required settings are allowed the required-field check in the
caller already refuses blanks on required fields before write.
"""
if not env_path.exists():
return []
values = _read_env(env_path)
errors: list[str] = []
for s in m.settings:
if s.type != "path":
continue
value = values.get(s.name, "")
if not value:
continue
p = Path(value)
if not p.is_absolute():
errors.append(f"{s.name}={value!r} must be an absolute path (start with /)")
continue
try:
resolved = p.resolve(strict=False)
except (OSError, RuntimeError) as e:
errors.append(f"{s.name}={value!r} cannot be resolved: {e}")
continue
if _is_denied_system_path(str(resolved)):
errors.append(f"{s.name}={value!r} resolves into a system path and is not allowed")
continue
if not resolved.exists():
errors.append(f"{s.name}={value!r} does not exist on this box")
continue
if not resolved.is_dir():
errors.append(f"{s.name}={value!r} is not a directory")
continue
return errors
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
def _format_env_value(v: str) -> str:
# Quote values that contain whitespace, quotes, or shell metacharacters so
# docker-compose's env substitution reads them back intact. Simple values
# stay unquoted to keep the file readable when a user SSHes in.
if v == "" or any(c in v for c in " \t\"'$`\\#"):
escaped = v.replace("\\", "\\\\").replace('"', '\\"')
return f'"{escaped}"'
return v
def write_env(env_path: Path, values: dict[str, str]) -> None:
"""Write a KEY=VALUE .env file atomically with 0600 perms.
Preserves insertion order of `values` so the file reads in the same order
the user filled in the form.
"""
lines = [f"{k}={_format_env_value(v)}" for k, v in values.items()]
body = "\n".join(lines) + ("\n" if lines else "")
tmp = env_path.with_suffix(env_path.suffix + ".tmp")
tmp.write_text(body)
tmp.chmod(0o600)
tmp.replace(env_path)
feat(furtka): reconciler + install/remove — slice 2 Fills in the act-on-it half of the resource manager. Reconciler walks the scanner output and brings docker into the desired state: ensures each manifest-declared volume exists (idempotent), then runs docker compose up -d for the project. install/remove on the CLI work end-to-end against a real /var/lib/furtka/apps/ tree. - furtka.dockerops: thin subprocess wrapper. Volume + compose primitives that other modules call. `_run` raises DockerError with the actual stderr so failures are diagnosable. - furtka.reconciler: builds an ordered Action list (volumes then compose_up per app), executes unless dry-run. Broken manifests produce a "skip" action, the rest of the apps still get reconciled. - furtka.installer: copy-from-source with two non-obvious rules — user .env is preserved across upgrade installs, and a missing .env is bootstrapped from .env.example so compose has values to substitute on first install. Bundled-app lookup falls back to /opt/furtka/apps/<name>/ when the source arg isn't a path. - furtka.cli: app install/remove wired up. remove() ignores compose down failures so a botched compose doesn't trap users with an un-removable folder. - 15 new tests using monkeypatch'd dockerops so the suite still runs without docker installed. Covers reconcile dry-run, multi-volume apps, broken-manifest skip behavior, .env preservation, bundled-name resolution, and remove edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:02:00 +02:00
def resolve_source(source: str) -> Path:
"""Resolve a `furtka app install <source>` arg to a real source folder.
If `source` looks like a path (or exists on disk), use it. Otherwise treat
feat(catalog): on-box apps catalog synced independently of core version New `furtka catalog sync` pulls the latest daniel/furtka-apps release, verifies its sha256, extracts under /var/lib/furtka/catalog/, and atomically swaps into place — so apps can ship without cutting a new Furtka core release. A daily timer (furtka-catalog-sync.timer, 10 min post-boot + 24 h with ±6 h jitter) drives the sync; /apps gets a manual "Sync apps catalog" button that kicks the same code path via a detached systemd-run unit. Layout of the new on-box tree: /var/lib/furtka/catalog/ synced catalog (survives self-updates) ├── VERSION └── apps/<name>/ ... /var/lib/furtka/catalog-state.json sync stage + last version, UI-polled /run/furtka/catalog.lock flock so timer + manual click can't race Resolver precedence (furtka/sources.py): catalog wins over the bundled seed (/opt/furtka/current/apps/, carried by the core release for offline first-boot). Installed apps under /var/lib/furtka/apps/ are never auto- swapped — user clicks Reinstall to move an existing install onto a newer catalog version; settings merge-preserved via the existing installer.install_from path. New files: - furtka/_release_common.py — shared Forgejo/tarball primitives lifted from furtka/updater.py. Both modules now import from here; updater's behaviour and public API unchanged. - furtka/catalog.py — check_catalog(), sync_catalog() with staging + manifest validation + atomic rename. Refuses bad sha256 / broken manifests and leaves the live catalog intact on any failure path. - furtka/sources.py — resolve_app_name() / list_available() abstraction used by installer.resolve_source and api._list_available. - assets/systemd/furtka-catalog-sync.{service,timer} — oneshot service + daily timer. Timer auto-enables on self-update via a one-line addition to _link_new_units (fresh installs get enabled via the webinstaller's _FURTKA_UNITS list). API + UI: - /api/bundled renamed internally to _list_available; endpoint stays as a backcompat alias; /api/apps/available is the new canonical name. Each list entry carries a `source` field ("catalog" | "bundled"). - POST /api/catalog/sync/check + /apply + GET /api/catalog/status. - /apps page grows a catalog-status row + Sync button; poll loop mirrors the Furtka self-update flow. CLI: `furtka catalog sync [--check]` + `furtka catalog status` (both support --json). Old `furtka app install` / `reconcile` / `update` / `rollback` surfaces are unchanged. Test gate: 194/170 baseline + 24 new tests covering catalog sync (happy path, sha256 mismatch, invalid manifest, lock contention, preserves-on-failure) + resolver precedence + api renames. ruff check + format clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:16:02 +02:00
it as an app name and look it up via `furtka.sources.resolve_app_name`
which checks the synced catalog first and falls back to the bundled seed.
feat(furtka): reconciler + install/remove — slice 2 Fills in the act-on-it half of the resource manager. Reconciler walks the scanner output and brings docker into the desired state: ensures each manifest-declared volume exists (idempotent), then runs docker compose up -d for the project. install/remove on the CLI work end-to-end against a real /var/lib/furtka/apps/ tree. - furtka.dockerops: thin subprocess wrapper. Volume + compose primitives that other modules call. `_run` raises DockerError with the actual stderr so failures are diagnosable. - furtka.reconciler: builds an ordered Action list (volumes then compose_up per app), executes unless dry-run. Broken manifests produce a "skip" action, the rest of the apps still get reconciled. - furtka.installer: copy-from-source with two non-obvious rules — user .env is preserved across upgrade installs, and a missing .env is bootstrapped from .env.example so compose has values to substitute on first install. Bundled-app lookup falls back to /opt/furtka/apps/<name>/ when the source arg isn't a path. - furtka.cli: app install/remove wired up. remove() ignores compose down failures so a botched compose doesn't trap users with an un-removable folder. - 15 new tests using monkeypatch'd dockerops so the suite still runs without docker installed. Covers reconcile dry-run, multi-volume apps, broken-manifest skip behavior, .env preservation, bundled-name resolution, and remove edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:02:00 +02:00
"""
p = Path(source)
if p.is_dir():
return p
if "/" in source or source.startswith("."):
raise InstallError(f"{source!r} is not a directory")
feat(catalog): on-box apps catalog synced independently of core version New `furtka catalog sync` pulls the latest daniel/furtka-apps release, verifies its sha256, extracts under /var/lib/furtka/catalog/, and atomically swaps into place — so apps can ship without cutting a new Furtka core release. A daily timer (furtka-catalog-sync.timer, 10 min post-boot + 24 h with ±6 h jitter) drives the sync; /apps gets a manual "Sync apps catalog" button that kicks the same code path via a detached systemd-run unit. Layout of the new on-box tree: /var/lib/furtka/catalog/ synced catalog (survives self-updates) ├── VERSION └── apps/<name>/ ... /var/lib/furtka/catalog-state.json sync stage + last version, UI-polled /run/furtka/catalog.lock flock so timer + manual click can't race Resolver precedence (furtka/sources.py): catalog wins over the bundled seed (/opt/furtka/current/apps/, carried by the core release for offline first-boot). Installed apps under /var/lib/furtka/apps/ are never auto- swapped — user clicks Reinstall to move an existing install onto a newer catalog version; settings merge-preserved via the existing installer.install_from path. New files: - furtka/_release_common.py — shared Forgejo/tarball primitives lifted from furtka/updater.py. Both modules now import from here; updater's behaviour and public API unchanged. - furtka/catalog.py — check_catalog(), sync_catalog() with staging + manifest validation + atomic rename. Refuses bad sha256 / broken manifests and leaves the live catalog intact on any failure path. - furtka/sources.py — resolve_app_name() / list_available() abstraction used by installer.resolve_source and api._list_available. - assets/systemd/furtka-catalog-sync.{service,timer} — oneshot service + daily timer. Timer auto-enables on self-update via a one-line addition to _link_new_units (fresh installs get enabled via the webinstaller's _FURTKA_UNITS list). API + UI: - /api/bundled renamed internally to _list_available; endpoint stays as a backcompat alias; /api/apps/available is the new canonical name. Each list entry carries a `source` field ("catalog" | "bundled"). - POST /api/catalog/sync/check + /apply + GET /api/catalog/status. - /apps page grows a catalog-status row + Sync button; poll loop mirrors the Furtka self-update flow. CLI: `furtka catalog sync [--check]` + `furtka catalog status` (both support --json). Old `furtka app install` / `reconcile` / `update` / `rollback` surfaces are unchanged. Test gate: 194/170 baseline + 24 new tests covering catalog sync (happy path, sha256 mismatch, invalid manifest, lock contention, preserves-on-failure) + resolver precedence + api renames. ruff check + format clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:16:02 +02:00
resolved = sources.resolve_app_name(source)
if resolved is None:
raise InstallError(f"{source!r} not found as a path, catalog app, or bundled app")
return resolved.path
feat(furtka): reconciler + install/remove — slice 2 Fills in the act-on-it half of the resource manager. Reconciler walks the scanner output and brings docker into the desired state: ensures each manifest-declared volume exists (idempotent), then runs docker compose up -d for the project. install/remove on the CLI work end-to-end against a real /var/lib/furtka/apps/ tree. - furtka.dockerops: thin subprocess wrapper. Volume + compose primitives that other modules call. `_run` raises DockerError with the actual stderr so failures are diagnosable. - furtka.reconciler: builds an ordered Action list (volumes then compose_up per app), executes unless dry-run. Broken manifests produce a "skip" action, the rest of the apps still get reconciled. - furtka.installer: copy-from-source with two non-obvious rules — user .env is preserved across upgrade installs, and a missing .env is bootstrapped from .env.example so compose has values to substitute on first install. Bundled-app lookup falls back to /opt/furtka/apps/<name>/ when the source arg isn't a path. - furtka.cli: app install/remove wired up. remove() ignores compose down failures so a botched compose doesn't trap users with an un-removable folder. - 15 new tests using monkeypatch'd dockerops so the suite still runs without docker installed. Covers reconcile dry-run, multi-volume apps, broken-manifest skip behavior, .env preservation, bundled-name resolution, and remove edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:02:00 +02:00
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
def install_from(src: Path, settings: dict[str, str] | None = None) -> Path:
feat(furtka): reconciler + install/remove — slice 2 Fills in the act-on-it half of the resource manager. Reconciler walks the scanner output and brings docker into the desired state: ensures each manifest-declared volume exists (idempotent), then runs docker compose up -d for the project. install/remove on the CLI work end-to-end against a real /var/lib/furtka/apps/ tree. - furtka.dockerops: thin subprocess wrapper. Volume + compose primitives that other modules call. `_run` raises DockerError with the actual stderr so failures are diagnosable. - furtka.reconciler: builds an ordered Action list (volumes then compose_up per app), executes unless dry-run. Broken manifests produce a "skip" action, the rest of the apps still get reconciled. - furtka.installer: copy-from-source with two non-obvious rules — user .env is preserved across upgrade installs, and a missing .env is bootstrapped from .env.example so compose has values to substitute on first install. Bundled-app lookup falls back to /opt/furtka/apps/<name>/ when the source arg isn't a path. - furtka.cli: app install/remove wired up. remove() ignores compose down failures so a botched compose doesn't trap users with an un-removable folder. - 15 new tests using monkeypatch'd dockerops so the suite still runs without docker installed. Covers reconcile dry-run, multi-volume apps, broken-manifest skip behavior, .env preservation, bundled-name resolution, and remove edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:02:00 +02:00
"""Copy a validated app folder into /var/lib/furtka/apps/<name>/.
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
If `settings` is provided, the .env is written from those values (this is
what the Web UI / API does user fills in a form, values land here).
Otherwise, preserves an existing .env on upgrade and bootstraps from
.env.example on first install.
Refuses to finish (raises InstallError) if the resulting .env still has
placeholder secrets the target folder is left in place so the user can
edit and re-run.
2026-04-15 10:17:00 +02:00
Returns the target folder on success.
feat(furtka): reconciler + install/remove — slice 2 Fills in the act-on-it half of the resource manager. Reconciler walks the scanner output and brings docker into the desired state: ensures each manifest-declared volume exists (idempotent), then runs docker compose up -d for the project. install/remove on the CLI work end-to-end against a real /var/lib/furtka/apps/ tree. - furtka.dockerops: thin subprocess wrapper. Volume + compose primitives that other modules call. `_run` raises DockerError with the actual stderr so failures are diagnosable. - furtka.reconciler: builds an ordered Action list (volumes then compose_up per app), executes unless dry-run. Broken manifests produce a "skip" action, the rest of the apps still get reconciled. - furtka.installer: copy-from-source with two non-obvious rules — user .env is preserved across upgrade installs, and a missing .env is bootstrapped from .env.example so compose has values to substitute on first install. Bundled-app lookup falls back to /opt/furtka/apps/<name>/ when the source arg isn't a path. - furtka.cli: app install/remove wired up. remove() ignores compose down failures so a botched compose doesn't trap users with an un-removable folder. - 15 new tests using monkeypatch'd dockerops so the suite still runs without docker installed. Covers reconcile dry-run, multi-volume apps, broken-manifest skip behavior, .env preservation, bundled-name resolution, and remove edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:02:00 +02:00
"""
manifest_path = src / "manifest.json"
if not manifest_path.exists():
raise InstallError(f"{src} has no manifest.json")
try:
m = load_manifest(manifest_path)
except ManifestError as e:
raise InstallError(str(e)) from e
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
if settings is not None:
declared = {s.name for s in m.settings}
for key in settings:
if key not in declared:
raise InstallError(f"{m.name}: unknown setting {key!r}")
feat(furtka): reconciler + install/remove — slice 2 Fills in the act-on-it half of the resource manager. Reconciler walks the scanner output and brings docker into the desired state: ensures each manifest-declared volume exists (idempotent), then runs docker compose up -d for the project. install/remove on the CLI work end-to-end against a real /var/lib/furtka/apps/ tree. - furtka.dockerops: thin subprocess wrapper. Volume + compose primitives that other modules call. `_run` raises DockerError with the actual stderr so failures are diagnosable. - furtka.reconciler: builds an ordered Action list (volumes then compose_up per app), executes unless dry-run. Broken manifests produce a "skip" action, the rest of the apps still get reconciled. - furtka.installer: copy-from-source with two non-obvious rules — user .env is preserved across upgrade installs, and a missing .env is bootstrapped from .env.example so compose has values to substitute on first install. Bundled-app lookup falls back to /opt/furtka/apps/<name>/ when the source arg isn't a path. - furtka.cli: app install/remove wired up. remove() ignores compose down failures so a botched compose doesn't trap users with an un-removable folder. - 15 new tests using monkeypatch'd dockerops so the suite still runs without docker installed. Covers reconcile dry-run, multi-volume apps, broken-manifest skip behavior, .env preservation, bundled-name resolution, and remove edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:02:00 +02:00
target = apps_dir() / m.name
target.mkdir(parents=True, exist_ok=True)
for item in src.iterdir():
if not item.is_file():
continue
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
# Never overwrite an existing user .env — either settings-driven write
# or previous manual edit has authority.
feat(furtka): reconciler + install/remove — slice 2 Fills in the act-on-it half of the resource manager. Reconciler walks the scanner output and brings docker into the desired state: ensures each manifest-declared volume exists (idempotent), then runs docker compose up -d for the project. install/remove on the CLI work end-to-end against a real /var/lib/furtka/apps/ tree. - furtka.dockerops: thin subprocess wrapper. Volume + compose primitives that other modules call. `_run` raises DockerError with the actual stderr so failures are diagnosable. - furtka.reconciler: builds an ordered Action list (volumes then compose_up per app), executes unless dry-run. Broken manifests produce a "skip" action, the rest of the apps still get reconciled. - furtka.installer: copy-from-source with two non-obvious rules — user .env is preserved across upgrade installs, and a missing .env is bootstrapped from .env.example so compose has values to substitute on first install. Bundled-app lookup falls back to /opt/furtka/apps/<name>/ when the source arg isn't a path. - furtka.cli: app install/remove wired up. remove() ignores compose down failures so a botched compose doesn't trap users with an un-removable folder. - 15 new tests using monkeypatch'd dockerops so the suite still runs without docker installed. Covers reconcile dry-run, multi-volume apps, broken-manifest skip behavior, .env preservation, bundled-name resolution, and remove edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:02:00 +02:00
if item.name == ".env" and (target / ".env").exists():
continue
shutil.copy2(item, target / item.name)
env = target / ".env"
env_example = target / ".env.example"
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
if settings is not None:
# Merge: start from existing .env (to preserve values the user didn't
# change — e.g. when editing a single password), overlay the submitted
# settings. Manifest-declared fields always appear in the final file.
existing = _read_env(env) if env.exists() else {}
merged: dict[str, str] = {}
for s in m.settings:
if s.name in settings:
merged[s.name] = settings[s.name]
elif s.name in existing:
merged[s.name] = existing[s.name]
elif s.default is not None:
merged[s.name] = s.default
else:
merged[s.name] = ""
# Preserve any non-manifest keys already in .env (forward-compat).
for k, v in existing.items():
if k not in merged:
merged[k] = v
# Required-field check runs on the merged view so that editing just
# one field (e.g. password) doesn't trip on unsubmitted fields that
# already have values in the existing .env.
for s in m.settings:
if s.required and not merged.get(s.name):
raise InstallError(f"{m.name}: setting {s.name!r} is required")
write_env(env, merged)
elif not env.exists() and env_example.exists():
# First install with no settings and no .env shipped: bootstrap from
# .env.example so compose has values to substitute.
feat(furtka): reconciler + install/remove — slice 2 Fills in the act-on-it half of the resource manager. Reconciler walks the scanner output and brings docker into the desired state: ensures each manifest-declared volume exists (idempotent), then runs docker compose up -d for the project. install/remove on the CLI work end-to-end against a real /var/lib/furtka/apps/ tree. - furtka.dockerops: thin subprocess wrapper. Volume + compose primitives that other modules call. `_run` raises DockerError with the actual stderr so failures are diagnosable. - furtka.reconciler: builds an ordered Action list (volumes then compose_up per app), executes unless dry-run. Broken manifests produce a "skip" action, the rest of the apps still get reconciled. - furtka.installer: copy-from-source with two non-obvious rules — user .env is preserved across upgrade installs, and a missing .env is bootstrapped from .env.example so compose has values to substitute on first install. Bundled-app lookup falls back to /opt/furtka/apps/<name>/ when the source arg isn't a path. - furtka.cli: app install/remove wired up. remove() ignores compose down failures so a botched compose doesn't trap users with an un-removable folder. - 15 new tests using monkeypatch'd dockerops so the suite still runs without docker installed. Covers reconcile dry-run, multi-volume apps, broken-manifest skip behavior, .env preservation, bundled-name resolution, and remove edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:02:00 +02:00
shutil.copy2(env_example, env)
2026-04-15 10:17:00 +02:00
# .env carries app secrets — lock to root-only. Done before the placeholder
# check so even the half-installed state is at least not world-readable.
if env.exists():
env.chmod(0o600)
bad = _placeholder_keys(env)
if bad:
raise InstallError(
f"{m.name}: {env} still has placeholder values for {', '.join(bad)}. "
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
f"Open the app in the Furtka UI to fill in real values, or edit the "
f"file and re-run `furtka app install {m.name}`."
2026-04-15 10:17:00 +02:00
)
path_errors = _path_setting_errors(m, env)
if path_errors:
raise InstallError(f"{m.name}: {'; '.join(path_errors)}")
feat(furtka): reconciler + install/remove — slice 2 Fills in the act-on-it half of the resource manager. Reconciler walks the scanner output and brings docker into the desired state: ensures each manifest-declared volume exists (idempotent), then runs docker compose up -d for the project. install/remove on the CLI work end-to-end against a real /var/lib/furtka/apps/ tree. - furtka.dockerops: thin subprocess wrapper. Volume + compose primitives that other modules call. `_run` raises DockerError with the actual stderr so failures are diagnosable. - furtka.reconciler: builds an ordered Action list (volumes then compose_up per app), executes unless dry-run. Broken manifests produce a "skip" action, the rest of the apps still get reconciled. - furtka.installer: copy-from-source with two non-obvious rules — user .env is preserved across upgrade installs, and a missing .env is bootstrapped from .env.example so compose has values to substitute on first install. Bundled-app lookup falls back to /opt/furtka/apps/<name>/ when the source arg isn't a path. - furtka.cli: app install/remove wired up. remove() ignores compose down failures so a botched compose doesn't trap users with an un-removable folder. - 15 new tests using monkeypatch'd dockerops so the suite still runs without docker installed. Covers reconcile dry-run, multi-volume apps, broken-manifest skip behavior, .env preservation, bundled-name resolution, and remove edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:02:00 +02:00
return target
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
def _read_env(env_path: Path) -> dict[str, str]:
"""Parse a simple KEY=VALUE .env into a dict. Unquotes quoted values."""
out: dict[str, str] = {}
for raw in env_path.read_text().splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
value = value.strip()
if (value.startswith('"') and value.endswith('"')) or (
value.startswith("'") and value.endswith("'")
):
value = value[1:-1].replace('\\"', '"').replace("\\\\", "\\")
out[key.strip()] = value
return out
def read_env_values(env_path: Path) -> dict[str, str]:
"""Public wrapper — returns {} if the file doesn't exist."""
if not env_path.exists():
return {}
return _read_env(env_path)
def update_env(name: str, settings: dict[str, str]) -> Path:
"""Merge `settings` into the installed app's .env.
Preserves values the user didn't submit. Validates required fields against
the merged view. Leaves files/manifest untouched for already-installed
apps only. Returns the target folder; caller is expected to run
reconcile to restart the containers.
"""
target = apps_dir() / name
manifest_path = target / "manifest.json"
if not manifest_path.exists():
raise InstallError(f"{name!r} is not installed")
try:
m = load_manifest(manifest_path)
except ManifestError as e:
raise InstallError(str(e)) from e
declared = {s.name for s in m.settings}
for key in settings:
if key not in declared:
raise InstallError(f"{m.name}: unknown setting {key!r}")
env = target / ".env"
existing = _read_env(env) if env.exists() else {}
merged: dict[str, str] = {}
for s in m.settings:
if s.name in settings:
merged[s.name] = settings[s.name]
elif s.name in existing:
merged[s.name] = existing[s.name]
elif s.default is not None:
merged[s.name] = s.default
else:
merged[s.name] = ""
for k, v in existing.items():
if k not in merged:
merged[k] = v
for s in m.settings:
if s.required and not merged.get(s.name):
raise InstallError(f"{m.name}: setting {s.name!r} is required")
write_env(env, merged)
bad = _placeholder_keys(env)
if bad:
raise InstallError(f"{m.name}: {env} still has placeholder values for {', '.join(bad)}.")
path_errors = _path_setting_errors(m, env)
if path_errors:
raise InstallError(f"{m.name}: {'; '.join(path_errors)}")
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
return target
feat(furtka): reconciler + install/remove — slice 2 Fills in the act-on-it half of the resource manager. Reconciler walks the scanner output and brings docker into the desired state: ensures each manifest-declared volume exists (idempotent), then runs docker compose up -d for the project. install/remove on the CLI work end-to-end against a real /var/lib/furtka/apps/ tree. - furtka.dockerops: thin subprocess wrapper. Volume + compose primitives that other modules call. `_run` raises DockerError with the actual stderr so failures are diagnosable. - furtka.reconciler: builds an ordered Action list (volumes then compose_up per app), executes unless dry-run. Broken manifests produce a "skip" action, the rest of the apps still get reconciled. - furtka.installer: copy-from-source with two non-obvious rules — user .env is preserved across upgrade installs, and a missing .env is bootstrapped from .env.example so compose has values to substitute on first install. Bundled-app lookup falls back to /opt/furtka/apps/<name>/ when the source arg isn't a path. - furtka.cli: app install/remove wired up. remove() ignores compose down failures so a botched compose doesn't trap users with an un-removable folder. - 15 new tests using monkeypatch'd dockerops so the suite still runs without docker installed. Covers reconcile dry-run, multi-volume apps, broken-manifest skip behavior, .env preservation, bundled-name resolution, and remove edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:02:00 +02:00
def remove(name: str) -> Path:
"""Delete /var/lib/furtka/apps/<name>/. Volumes are NOT touched.
Caller is responsible for stopping the compose project first.
"""
target = apps_dir() / name
if not target.exists():
raise InstallError(f"{name!r} is not installed")
shutil.rmtree(target)
return target