From a3a993716b3b55412c186468260687370ffbb4ff Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Sun, 11 Oct 2026 12:31:05 -0400 Subject: [PATCH] choices: name a choise's jail as the compose names it's service --- dbuild/appjail_choices.py | 7 ++- dbuild/choices.py | 10 ++-- dbuild/docs.py | 6 +++ dbuild/templates/appjail-director.yml.j2 | 2 +- tests/test_appjail_choices.py | 4 ++ tests/test_docs.py | 60 +++++++++++++++++++++++- 6 files changed, 79 insertions(+), 10 deletions(-) diff --git a/dbuild/appjail_choices.py b/dbuild/appjail_choices.py index 76ed651..e28d65f 100644 --- a/dbuild/appjail_choices.py +++ b/dbuild/appjail_choices.py @@ -90,10 +90,9 @@ def option_forms(cfg: Any) -> dict[str, dict[str, dict[str, Any]]]: added = _added(_director_dict(_render_template_director(cfg, with_dep)), base) if o.jail_template_file and o.jail_template: files[o.jail_template_file] = o.jail_template - # The jail's own service name in the director: what fjord - # names the jail by, and so what the app's host variable - # must say there (the compose calls it "mariadb", the - # director "vikunja-mariadb"). + # The jail's service name in the director, the compose's + # ("mariadb"): what fjord names the jail by, and so what the + # app's host variable must say there. for var in o.hostnames.values(): hostnames[o.director_dep["name"]] = var if not added: diff --git a/dbuild/choices.py b/dbuild/choices.py index 75b10dc..344f1f5 100644 --- a/dbuild/choices.py +++ b/dbuild/choices.py @@ -197,16 +197,18 @@ def _database_options(cid: str, c: dict[str, Any], services: dict[str, Any]) -> f" - \"${{{v_data}}}:{e['data']}\"\n" ) env_lines = [f"{v_user}={app}", f"{v_pass}= # set one", f"{v_name}={app}", f"{v_data}=/containers/{app}/{e['service']}"] - # appjail-director: the service is a jail named _, which - # is also its hostname on the project's network; it starts before the - # app (director starts by ascending priority, default 99). + # appjail-director: the service is named as in the compose ("mariadb"), + # so a plan that names compose services (fjord's networking) finds it + # on AppJail too; the jail is _, unique on the host and + # its hostname on the project's network. It starts before the app + # (director starts by ascending priority, default 99). jail = f"{app}-{e['service']}" tpl_file = f"{e['service']}-template.conf" if e.get("jail_template") else "" dep_env = {e["env"]["user"]: f"!ENV '${{{v_user}}}'", e["env"]["password"]: f"!ENV '${{{v_pass}}}'", e["env"]["name"]: f"!ENV '${{{v_name}}}'"} for k, v in (e.get("extra_env") or {}).items(): dep_env[k] = "!ENV '" + v.format(password=v_pass) + "'" - director_dep = {"name": jail, "image": e["image"], "template": tpl_file, "priority": 10, + director_dep = {"name": e["service"], "jail": jail, "image": e["image"], "template": tpl_file, "priority": 10, "env": dep_env, "volumes": {cid: e["data"]}} env_appjail = {v_host: jail.replace("-", "_")} if v_host else {} opts.append(Option(id=eid, label=e["label"], doc=e["doc"], env=env, service_yaml=service_yaml, env_lines=env_lines, diff --git a/dbuild/docs.py b/dbuild/docs.py index 65d8e49..33023bb 100644 --- a/dbuild/docs.py +++ b/dbuild/docs.py @@ -450,6 +450,12 @@ def _choice_context(cfg: Config, context_env: list[dict[str, Any]]) -> dict[str, for o in opts: if o.director_dep: aj["depends_on"] = [*(aj.get("depends_on") or []), o.director_dep] + # A sidecar is named as its compose service, so an option that drops + # the service (grimmory's own MariaDB) drops the jail too. + dropped = {d for o in opts for d in o.drop} + if dropped and aj.get("depends_on"): + aj["depends_on"] = [d for d in aj["depends_on"] if d.get("name") not in dropped] + for o in opts: values.update(o.env) values.update(o.env_appjail) for ln in o.env_lines: diff --git a/dbuild/templates/appjail-director.yml.j2 b/dbuild/templates/appjail-director.yml.j2 index 78eef25..00bf5eb 100644 --- a/dbuild/templates/appjail-director.yml.j2 +++ b/dbuild/templates/appjail-director.yml.j2 @@ -63,7 +63,7 @@ services: {%- if appjail and appjail.depends_on %} {%- for dep in appjail.depends_on %} {{ dep.name }}: - name: {{ dep.name | replace("-", "_") }} + name: {{ (dep.jail or dep.name) | replace("-", "_") }} {%- if dep.priority %} priority: {{ dep.priority }} {%- endif %} diff --git a/tests/test_appjail_choices.py b/tests/test_appjail_choices.py index 5b82b84..655a30e 100644 --- a/tests/test_appjail_choices.py +++ b/tests/test_appjail_choices.py @@ -41,6 +41,10 @@ def test_a_database_option_brings_its_jail(self): # The host variable names the jail by its director service. (svc, var), = pg["hostnames"].items() self.assertEqual(var, "TODO_DB_HOST") + # Named as the compose names it, so fjord's network plan (which names + # compose services) finds the jail; grafana's MariaDB was refused. + self.assertEqual(svc, "postgres") + self.assertIn("name: todo_postgres", pg["director"], "the jail stays app-prefixed") self.assertIn(f" {svc}:\n", pg["director"]) self.assertNotIn("sqlite", forms.get("database", {}), "nothing to add, no form") diff --git a/tests/test_docs.py b/tests/test_docs.py index e9686f2..c7bb111 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -262,6 +262,64 @@ def test_single_service_unchanged(self): self.assertEqual(dbuild_config.load(repo).compose_text, "") +class TestReadmeDroppedSidecar(unittest.TestCase): + """An option that drops the bundled database drops its jail too: the + README's director for "your own MariaDB" ran a MariaDB nobody wanted + (grimmory, 2026-10-11).""" + + COMPOSE = ( + "name: app\n" + "x-daemonless:\n" + ' title: "App"\n' + ' description: "An app and its database."\n' + ' category: "Productivity"\n' + ' user: "bsd"\n' + " appjail:\n" + " depends_on:\n" + " - name: mariadb\n" + " image: ghcr.io/daemonless/mariadb:latest\n" + " choices:\n" + " database:\n" + " label: Database\n" + " default: builtin\n" + " options:\n" + " builtin:\n" + " label: Built-in MariaDB\n" + " external:\n" + " label: Your own MariaDB\n" + " drop: [mariadb]\n" + " ask:\n" + " - name: DB_HOST\n" + " label: Host\n" + "services:\n" + " app:\n" + " image: ghcr.io/daemonless/app:latest\n" + " environment:\n" + " - DB_HOST=${DB_HOST:-127.0.0.1}\n" + " depends_on:\n" + " - mariadb\n" + " mariadb:\n" + " image: ghcr.io/daemonless/mariadb:latest\n" + ) + + def test_director_without_the_dropped_jail(self): + with tempfile.TemporaryDirectory() as d: + repo = Path(d) + (repo / ".daemonless").mkdir() + (repo / "compose.yaml").write_text(self.COMPOSE) + (repo / "example.env").write_text("DB_HOST=\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) + self.assertEqual(errors, []) + aj = outputs["README.md"].split("### AppJail Director", 1)[1] + builtin = aj[aj.index("#### Built-in MariaDB"):aj.index("#### Your own MariaDB")] + own = aj[aj.index("#### Your own MariaDB"):] + self.assertIn(" mariadb:\n", builtin, "the default keeps its database") + self.assertNotIn(" mariadb:\n", own, "your own MariaDB: no jail for the bundled one") + self.assertIn("container: 'args:--pull'", own, "the app's own jail is still there") + @contextlib.contextmanager def _set_env(key: str, value: str): prev = os.environ.get(key) @@ -500,7 +558,7 @@ def test_director_per_option_from_the_same_engine_table(self): readme = outputs["README.md"] aj = readme[readme.index("### AppJail Director"):] pg = aj[aj.index("#### PostgreSQL, Off"):aj.index("#### MariaDB, Off")] - self.assertIn(" todo-postgres:\n name: todo_postgres\n priority: 10", pg, "the engine's jail starts first") + self.assertIn(" postgres:\n name: todo_postgres\n priority: 10", pg, "named as the compose service, the jail app-prefixed, starting first") self.assertIn("POSTGRES_PASSWORD: !ENV '${TODO_DB_PASSWORD}'", pg, "reads the app's own variables") self.assertIn("TODO_DB_HOST=todo_postgres", pg, "the host is the jail's name") self.assertIn("template: !ENV '${PWD}/postgres-template.conf'", pg)