Compare commits

..

2 commits

Author SHA1 Message Date
1155f1d4ba chore: release 26.18-alpha
All checks were successful
Build ISO / build-iso (push) Successful in 18m44s
Deploy site / deploy (push) Successful in 4s
CI / lint (push) Successful in 28s
CI / test (push) Successful in 1m28s
CI / validate-json (push) Successful in 24s
CI / markdown-links (push) Successful in 14s
Release / release (push) Successful in 12m5s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:38:54 +02:00
1a2b817eb8 fix(deps): give on_start hooks the consumer's stored credentials
All checks were successful
CI / lint (pull_request) Successful in 31s
CI / test (pull_request) Successful in 1m27s
CI / validate-json (pull_request) Successful in 25s
CI / markdown-links (pull_request) Successful in 54s
Build ISO / build-iso (push) Successful in 18m35s
CI / lint (push) Successful in 29s
CI / test (push) Successful in 1m20s
CI / validate-json (push) Successful in 25s
CI / markdown-links (push) Successful in 54s
The reconciler handed an on_start dependency hook only
FURTKA_CONSUMER_APP/_VERSION, so it had no way to learn the consumer's
existing secrets. That made the feature's own headline use case —
re-create a provider account (e.g. an MQTT user) after a wipe with the
same password the consumer already holds — impossible unless the
provider stashed a copy itself.

on_start now also receives the consumer's .env values, namespaced under
FURTKA_CONSUMER_ENV_<KEY> (UPPER_SNAKE_CASE keys only, so a hand-edited
.env can't produce a malformed --env arg). on_start stays read-only
toward the consumer: unlike on_install, its stdout is intentionally not
merged back into the consumer's .env.

Surfaced building the first real provider/consumer catalog pair
(mosquitto + zigbee2mqtt) in daniel/furtka-apps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:45:42 +02:00
7 changed files with 82 additions and 10 deletions

View file

@ -7,6 +7,25 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r
## [Unreleased]
## [26.18-alpha] - 2026-06-04
### Fixed
- **`on_start` dependency hooks now receive the consumer's stored
credentials.** Previously the reconciler handed an `on_start` hook only
`FURTKA_CONSUMER_APP`/`FURTKA_CONSUMER_VERSION`, so it had no way to learn
the consumer's existing secrets — which made the feature's own headline use
case (re-create a provider account, e.g. an MQTT user, after a wipe, with
the *same* password the consumer already holds) impossible without the
provider stashing a copy itself. The hook now also gets the consumer's `.env`
values, namespaced under `FURTKA_CONSUMER_ENV_<KEY>` (only UPPER_SNAKE_CASE
keys, so a hand-edited `.env` can't produce a malformed `--env` argument).
`on_start` stays read-only with respect to the consumer: unlike `on_install`,
its stdout is intentionally not merged back into the consumer's `.env` — it
reads consumer state to reconcile provider state, it doesn't mutate it.
Surfaced by building the first real provider/consumer catalog pair
(mosquitto + zigbee2mqtt) in daniel/furtka-apps.
## [26.17-alpha] - 2026-05-11
### Added

View file

@ -1,12 +1,17 @@
from dataclasses import dataclass
from pathlib import Path
from furtka import deps, dockerops
from furtka.manifest import ManifestError, load_manifest
from furtka import deps, dockerops, installer
from furtka.manifest import SETTING_NAME_RE, ManifestError, load_manifest
from furtka.scanner import scan
_ON_START_TIMEOUT_SECONDS = 30.0
# Consumer .env values are exported into an on_start hook's environment under
# this prefix. Namespaced so a consumer setting named PATH/HOME/etc. can't
# clobber the provider container's own environment when the hook runs.
_CONSUMER_ENV_PREFIX = "FURTKA_CONSUMER_ENV_"
@dataclass(frozen=True)
class Action:
@ -106,13 +111,33 @@ def _fire_on_start_hook(consumer, req, apps_root: Path) -> None:
req.app,
service,
hook_abs,
env={
"FURTKA_CONSUMER_APP": consumer.name,
"FURTKA_CONSUMER_VERSION": consumer.version,
},
env=_on_start_hook_env(consumer, apps_root),
timeout=_ON_START_TIMEOUT_SECONDS,
)
def _on_start_hook_env(consumer, apps_root: Path) -> dict[str, str]:
"""Build the environment handed to an on_start hook.
Beyond the consumer's name/version, this injects the consumer's stored
`.env` values (namespaced under `FURTKA_CONSUMER_ENV_`) so the hook can
re-establish provider-side state *idempotently* e.g. re-create an MQTT
account with the SAME password the consumer already holds. The reconciler
does not merge an on_start hook's stdout back into the consumer's `.env`
(unlike on_install): on_start reads consumer state, it doesn't mutate it.
Only UPPER_SNAKE_CASE keys are injected so a hand-edited `.env` can't
produce a malformed `docker compose exec --env` argument.
"""
env = {
"FURTKA_CONSUMER_APP": consumer.name,
"FURTKA_CONSUMER_VERSION": consumer.version,
}
consumer_env = installer.read_env_values(apps_root / consumer.name / ".env")
for key, value in consumer_env.items():
if SETTING_NAME_RE.match(key):
env[f"{_CONSUMER_ENV_PREFIX}{key}"] = value
return env
def has_errors(actions: list[Action]) -> bool:
return any(a.kind == "error" for a in actions)

View file

@ -1,6 +1,6 @@
[project]
name = "furtka"
version = "26.17-alpha"
version = "26.18-alpha"
description = "Open-source home server OS — simple enough for everyone."
requires-python = ">=3.11"
readme = "README.md"

View file

@ -196,6 +196,34 @@ def test_reconcile_fires_on_start_before_compose_up(tmp_path, fake_docker, monke
assert fake_docker["compose_up"][0][1] == "mosquitto"
def test_reconcile_on_start_hook_receives_consumer_env(tmp_path, fake_docker, monkeypatch):
# The on_start hook must be able to re-establish provider state with the
# SAME credentials the consumer already holds, so the consumer's stored
# .env is injected (namespaced) alongside FURTKA_CONSUMER_APP/_VERSION.
provider = _make_app(tmp_path, "mosquitto", PROVIDER_MANIFEST)
(provider / "hooks").mkdir()
(provider / "hooks" / "ensure-user.sh").write_bytes(b"#!/bin/sh\n")
consumer = _make_app(tmp_path, "zigbee2mqtt", CONSUMER_MANIFEST)
# A real, stored credential plus a junk lowercase key that must be dropped.
(consumer / ".env").write_text("MQTT_PASS=s3cret-from-install\nnot_a_setting=x\n")
seen_env: dict[str, str] = {}
def fake_exec_script(app_dir, project, service, script_path, *, env, timeout):
seen_env.update(env)
return ""
monkeypatch.setattr(dockerops, "compose_image_tags", lambda a, p: {"mosquitto": "img"})
monkeypatch.setattr(dockerops, "compose_exec_script", fake_exec_script)
reconciler.reconcile(tmp_path)
assert seen_env["FURTKA_CONSUMER_APP"] == "zigbee2mqtt"
assert seen_env["FURTKA_CONSUMER_ENV_MQTT_PASS"] == "s3cret-from-install"
# Lowercase / non-UPPER_SNAKE keys are not exported.
assert not any(k.endswith("not_a_setting") for k in seen_env)
def test_reconcile_on_start_failure_skips_consumer_compose_up(tmp_path, fake_docker, monkeypatch):
provider = _make_app(tmp_path, "mosquitto", PROVIDER_MANIFEST)
(provider / "hooks").mkdir()

View file

@ -1,7 +1,7 @@
---
title: "Furtka"
description: "Offenes Heimserver-Betriebssystem — einfach genug für alle."
status: "<span class=\"mono\">26.16-alpha</span> — in Arbeit"
status: "<span class=\"mono\">26.18-alpha</span> — in Arbeit"
# features_today / features_next müssen index-parallel zu content/_index.md bleiben.
intro: |
**Furtka** ist ein offenes Heimserver-Betriebssystem.

View file

@ -1,7 +1,7 @@
---
title: "Furtka"
description: "Open-source home server OS — simple enough for everyone."
status: "<span class=\"mono\">26.16-alpha</span> — work in progress"
status: "<span class=\"mono\">26.18-alpha</span> — work in progress"
# Keep features_today / features_next index-aligned with content/_index.de.md.
intro: |
**Furtka** is an open-source home server OS.

View file

@ -6,7 +6,7 @@ enableRobotsTXT = true
[params]
description = "Open-source home server OS — simple enough for everyone."
version = "26.16-alpha"
version = "26.18-alpha"
contactEmail = "hallo@furtka.org"
[markup.goldmark.renderer]