Compare commits
2 commits
26.17-alph
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1155f1d4ba | |||
| 1a2b817eb8 |
7 changed files with 82 additions and 10 deletions
19
CHANGELOG.md
19
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue