From b38ff64acf27ad6798eb5b310622d08e51949aea Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Wed, 14 Oct 2026 04:51:23 -0400 Subject: [PATCH] fix(appjail): the jail's .env keeps a ${VAR} reference --- dbuild/docs.py | 14 ++++++++++++++ dbuild/templates/appjail.env.j2 | 2 +- tests/test_docs.py | 27 +++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/dbuild/docs.py b/dbuild/docs.py index 20e293c..d64c44d 100644 --- a/dbuild/docs.py +++ b/dbuild/docs.py @@ -713,6 +713,7 @@ def _enrich_metadata(cfg: Config, community_override: str | None = None) -> dict name = e["name"] val = e["default"] + ref = None if cfg.compose_text: ref = re.fullmatch(r"\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}", str(val or "")) if ref: @@ -734,9 +735,22 @@ def _enrich_metadata(cfg: Config, community_override: str | None = None) -> dict # STUMP_TRUST_PROXY_HEADERS="" and crash-loops on AppJail only. if e.get("docs_only") and not val: item["unset"] = True + # The README shows the value; the jail's .env keeps the reference, or + # it loses the link: grimmory's DATABASE_PASSWORD=${DB_PASSWORD} went + # out as "grimmory" while mariadb got the password fjord generated. + if ref: + item["ref"] = ref.group(0) if name in ["PUID", "PGID", "TZ"]: item["placeholder"] = f"@{name}@" context["env"].append(item) + # Only a reference the .env can answer, to another variable: one it + # doesn't carry expands to nothing, and TZ=${TZ:-Etc/UTC} names itself. + carried = {i["name"] for i in context["env"] if not i.get("unset")} + for i in context["env"]: + ref = i.pop("ref", "") + target = re.match(r"\$\{([A-Za-z_][A-Za-z0-9_]*)", ref).group(1) if ref else "" + if target and target != i["name"] and target in carried: + i["env_ref"] = ref # Volumes vol_docs = {str(k): v for k, v in docs.get("volumes", {}).items()} diff --git a/dbuild/templates/appjail.env.j2 b/dbuild/templates/appjail.env.j2 index 98ee749..a8c2fc4 100644 --- a/dbuild/templates/appjail.env.j2 +++ b/dbuild/templates/appjail.env.j2 @@ -2,7 +2,7 @@ DIRECTOR_PROJECT={{ name }} {%- for e in env if not e.unset %} -{{ e.name }}={{ e.placeholder if (render_mode is defined and render_mode == "mkdocs" and e.placeholder) else e.default }} +{{ e.name }}={{ e.placeholder if (render_mode is defined and render_mode == "mkdocs" and e.placeholder) else (e.env_ref or e.default) }} {%- endfor %} {%- if render_mode is defined and render_mode == "deploy" %} {%- for p in ports %} diff --git a/tests/test_docs.py b/tests/test_docs.py index 83f0b11..8b69d66 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -386,6 +386,33 @@ def test_jail_is_named_as_the_compose_service(self): director = (Path(d) / "out" / "appjail-director.yml").read_text() self.assertIn("\n radicale:\n name: radicale\n", director) + def test_jail_env_keeps_a_reference_to_another_variable(self): + # grimmory: DATABASE_PASSWORD=${DB_PASSWORD} went into the jail's .env + # as example.env's "grimmory" while mariadb got the password fjord + # generated -- access denied. The README still shows the value. + compose = ( + self.COMPOSE.replace(' TZ: "Timezone"\n', ' TZ: "Timezone"\n DB_PASSWORD: "Database password"\n') + .replace(" - TZ=UTC\n", " - TZ=${TZ:-UTC}\n - DATABASE_PASSWORD=${DB_PASSWORD}\n - DB_PASSWORD=${DB_PASSWORD}\n - LOST=${NOT_IN_ENV}\n") + + " db:\n image: ghcr.io/daemonless/mariadb:latest\n" + ) # a stack: a single image's values are never resolved + with tempfile.TemporaryDirectory() as d: + repo = Path(d) / "app" + (repo / ".daemonless").mkdir(parents=True) + (repo / "compose.yaml").write_text(compose) + (repo / "example.env").write_text("DB_PASSWORD=app\nNOT_IN_ENV=x\n") + (repo / "Containerfile.j2").write_text(CONTAINERFILE_J2) + with _chdir(repo): + cfg = dbuild_config.load(repo) + outputs, errors = docs.render_generated(cfg, argparse.Namespace(community=None), repo) + docs.generate_appjail_files(cfg, Path(d) / "out") + env = next(p for p in (Path(d) / "out").iterdir() if p.name.endswith("env")).read_text() + self.assertEqual(errors, []) + self.assertIn("DATABASE_PASSWORD=${DB_PASSWORD}\n", env) + self.assertIn("DB_PASSWORD=app\n", env, "the variable it names keeps its value") + self.assertIn("TZ=UTC\n", env, "a variable never names itself") + self.assertNotIn("${NOT_IN_ENV}", env, "a name the .env doesn't carry keeps the value") + self.assertIn("| `DATABASE_PASSWORD` | `app` |", outputs["README.md"], "the README table shows the value") + @contextlib.contextmanager def _set_env(key: str, value: str): prev = os.environ.get(key)