Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions dbuild/appjail_choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 6 additions & 4 deletions dbuild/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <app>_<engine>, 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 <app>_<engine>, 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,
Expand Down
6 changes: 6 additions & 0 deletions dbuild/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion dbuild/templates/appjail-director.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
4 changes: 4 additions & 0 deletions tests/test_appjail_choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
60 changes: 59 additions & 1 deletion tests/test_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading