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]
|
## [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
|
## [26.17-alpha] - 2026-05-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from furtka import deps, dockerops
|
from furtka import deps, dockerops, installer
|
||||||
from furtka.manifest import ManifestError, load_manifest
|
from furtka.manifest import SETTING_NAME_RE, ManifestError, load_manifest
|
||||||
from furtka.scanner import scan
|
from furtka.scanner import scan
|
||||||
|
|
||||||
_ON_START_TIMEOUT_SECONDS = 30.0
|
_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)
|
@dataclass(frozen=True)
|
||||||
class Action:
|
class Action:
|
||||||
|
|
@ -106,13 +111,33 @@ def _fire_on_start_hook(consumer, req, apps_root: Path) -> None:
|
||||||
req.app,
|
req.app,
|
||||||
service,
|
service,
|
||||||
hook_abs,
|
hook_abs,
|
||||||
env={
|
env=_on_start_hook_env(consumer, apps_root),
|
||||||
"FURTKA_CONSUMER_APP": consumer.name,
|
|
||||||
"FURTKA_CONSUMER_VERSION": consumer.version,
|
|
||||||
},
|
|
||||||
timeout=_ON_START_TIMEOUT_SECONDS,
|
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:
|
def has_errors(actions: list[Action]) -> bool:
|
||||||
return any(a.kind == "error" for a in actions)
|
return any(a.kind == "error" for a in actions)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "furtka"
|
name = "furtka"
|
||||||
version = "26.17-alpha"
|
version = "26.18-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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
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):
|
def test_reconcile_on_start_failure_skips_consumer_compose_up(tmp_path, fake_docker, monkeypatch):
|
||||||
provider = _make_app(tmp_path, "mosquitto", PROVIDER_MANIFEST)
|
provider = _make_app(tmp_path, "mosquitto", PROVIDER_MANIFEST)
|
||||||
(provider / "hooks").mkdir()
|
(provider / "hooks").mkdir()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
title: "Furtka"
|
title: "Furtka"
|
||||||
description: "Offenes Heimserver-Betriebssystem — einfach genug für alle."
|
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.
|
# features_today / features_next müssen index-parallel zu content/_index.md bleiben.
|
||||||
intro: |
|
intro: |
|
||||||
**Furtka** ist ein offenes Heimserver-Betriebssystem.
|
**Furtka** ist ein offenes Heimserver-Betriebssystem.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
title: "Furtka"
|
title: "Furtka"
|
||||||
description: "Open-source home server OS — simple enough for everyone."
|
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.
|
# Keep features_today / features_next index-aligned with content/_index.de.md.
|
||||||
intro: |
|
intro: |
|
||||||
**Furtka** is an open-source home server OS.
|
**Furtka** is an open-source home server OS.
|
||||||
|
|
|
||||||
|
|
@ -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.16-alpha"
|
version = "26.18-alpha"
|
||||||
contactEmail = "hallo@furtka.org"
|
contactEmail = "hallo@furtka.org"
|
||||||
|
|
||||||
[markup.goldmark.renderer]
|
[markup.goldmark.renderer]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue