Cut the comment bloat from the Podman playbooks

Comment blocks now stay within three lines and explain only the non-obvious. Long histories, include examples, and commented-out code are gone. No task, key, or value changed; the only data change is comment text inside the generated unit files.
This commit is contained in:
tanshu committed 2026-10-18 22:10:57 +05:30
1 parent 2aab1aec1e
commit 476a6d7cd0
85 files changed
+372 -2433

No files matched your search

+2 -38
View File
@@ -1,28 +1,5 @@
---
# Tandoor Recipes as a rootless Podman Quadlet unit, behind the shared Caddy
# unit (see caddy/playbook.yml) and the shared Postgres unit
# (postgres/playbook.yml).
#
# Connects as the container user and runs everything as that user: the Quadlet
# units belong to it, so the connection's PAM session is what makes
# `systemctl --user` work without any environment juggling.
#
# gather_facts is required: podman_user and the home directory come from the
# user_id / user_dir facts.
#
# The rootless-Podman prerequisites (installing podman, enabling lingering, the
# low-port sysctl, creating the Quadlet directory) are deliberately NOT applied
# here. They are the shared task file at caddy/roles/podman/tasks/main.yaml, and
# a host that has already run caddy/playbook.yml has them:
#
# - name: Rootless Podman prerequisites
# ansible.builtin.include_tasks: "{{ playbook_dir }}/../caddy/roles/podman/tasks/main.yaml"
#
# The variables this play needs are duplicated at the top of vars/default.yml, so
# it stands on its own. (Applying the commented include above would also need
# podman_unprivileged_port_start, which lives in caddy/vars/default.yml.)
#
# Run with: ansible-playbook -u tanshu tandoor/playbook.yml
# Requires caddy and postgres to have run first.
- name: Deploy Tandoor as a rootless Podman Quadlet
hosts: emily
gather_facts: true
@@ -30,24 +7,11 @@
- vars/default.yml
pre_tasks:
# This must happen before the app unit is started, not after: the generated
# unit references {{ network_name }}.network, and Quadlet fails *unit
# generation* (so `systemctl --user start` reports "unit not found") when a
# referenced .network file does not exist yet. The same include writes the
# Caddy-side drop-in that attaches Caddy to this network, and the Postgres
# attach below needs the network unit to exist as well.
#
# network_name is a play var, so the include can see it directly.
# Quadlet needs the network unit before it generates the app unit.
- name: Register Tandoor's network with Caddy
ansible.builtin.include_tasks: "{{ playbook_dir }}/../caddy/roles/register/tasks/main.yaml"
roles:
# Attaches this app's network to the shared Postgres unit, which is what
# makes POSTGRES_HOST=db resolve, and owns the "Restart Postgres" handler
# that postgres/roles/attach notifies. Runs first so the database is
# reachable before the app starts.
- postgres
# The app: data directories, .env, the Quadlet unit, start + verify.
- tandoor
# Appends this site's block to the shared Caddyfile and reloads Caddy.
- caddy
+4 -17
View File
@@ -1,27 +1,14 @@
---
# Caddy is its own playbook (caddy/playbook.yml) and a rootless Quadlet unit
# there. These handlers run as the connection user, so `systemctl --user` and
# `podman exec` work without any XDG_RUNTIME_DIR juggling.
# Notified by this role when the Caddyfile changes. A reload keeps existing
# connections; a restart would briefly drop every hosted site.
#
# Note the config path: /etc/caddy/Caddyfile is the mount inside the container,
# not {{ caddyfile_path }}, which is the host side of it.
# A reload keeps existing connections. A restart would briefly drop every hosted site.
# The config path is inside the container, not caddyfile_path on the host.
- name: Reload Caddy configuration
ansible.builtin.command: >-
podman exec {{ caddy_name }} caddy reload --config /etc/caddy/Caddyfile
changed_when: true
listen: "Reload Caddy"
# Notified by caddy/roles/register when this service's network unit, or the
# drop-in that attaches Caddy to it, is written. Quadlet has no live equivalent
# of `docker network connect`, so the container has to be recreated — a brief
# restart of every hosted site, not just this one. See caddy/readme.md,
# "Trade-off worth knowing".
#
# Defined after the reload above so that when both are notified the cheap
# config reload happens first.
# Quadlet has no live equivalent of network connect, so Caddy is recreated and every hosted site restarts.
# Defined after the reload so the cheap reload runs first.
- name: Restart Caddy
ansible.builtin.systemd_service:
name: "{{ caddy_name }}.service"
-11
View File
@@ -1,15 +1,4 @@
---
# Append this site's block to the shared Caddyfile.
#
# The Caddyfile is a bind mount into the Caddy container, so Caddy sees the
# change as soon as the file is written; the handler only asks Caddy to re-read
# it. That is a reload, not a restart, so the other hosted sites are untouched.
#
# blockinfile is idempotent on its own via the marker, so there is no need to
# slurp the file and compare first — which also matters because slurp *fails*
# when the Caddyfile does not exist yet, on a host where the Caddy playbook has
# not run.
- name: "Add the {{ host }} site block to the Caddyfile"
ansible.builtin.blockinfile:
path: "{{ caddyfile_path }}"
+1 -9
View File
@@ -1,13 +1,5 @@
---
# Postgres is its own playbook (postgres/playbook.yml) and a rootless Quadlet
# unit there. This handler runs as the connection user, so `systemctl --user`
# works without setting XDG_RUNTIME_DIR.
#
# Required by postgres/roles/attach, which notifies it whenever this app's
# drop-in changes: a new drop-in changes the generated unit, so Postgres must be
# recreated to join the network — Quadlet has no live equivalent of
# `podman network connect`. That is a few seconds of downtime for every attached
# app, not just this one. See postgres/readme.md, "The trade-off".
# A new drop-in changes the generated unit, so Postgres is recreated and every attached app restarts.
- name: Restart Postgres
ansible.builtin.systemd_service:
name: "{{ postgres_unit }}"
+1 -20
View File
@@ -1,26 +1,7 @@
---
# Attach this app's network to the shared Postgres unit.
#
# Postgres knows nothing about its clients, exactly like Caddy: a service joins
# by dropping 50-tandoor.conf and the shared 10-network-aliases.conf next to
# postgres.container. The canonical way to do that is the shared include below —
# it writes the same bytes every service writes, asserts that tandoor.network
# already exists, and notifies the "Restart Postgres" handler defined in this
# role.
#
# This runs in the roles phase, which is after the register include in
# playbook.yml's pre_tasks has created tandoor.network.
- name: Attach Tandoor to Postgres
ansible.builtin.include_tasks: "{{ playbook_dir }}/../postgres/roles/attach/tasks/main.yaml"
# On a first attach both drop-ins are new, so Postgres has to be recreated to
# join tandoor_net — and that restart is a *handler*, which by default runs at
# the end of the play, after the tandoor role has already started the app. The
# app would then come up before `db` resolves, fail its own is-active check, and
# fail the play on a machine where nothing is actually wrong.
#
# Flushing applies the new drop-in before the app starts. On later runs the
# drop-ins are unchanged, nothing is pending, and this is a no-op.
# The restart that joins Postgres to tandoor_net is a handler, so flush it before the app starts or its is-active check fails.
- name: Apply the Postgres network change before starting Tandoor
ansible.builtin.meta: flush_handlers
+7 -28
View File
@@ -1,10 +1,4 @@
---
# Tandoor Recipes as a rootless Podman Quadlet unit.
#
# Everything here runs as {{ podman_user }} — the connection user. No become, no
# XDG_RUNTIME_DIR: the SSH connection gave us a real PAM session, so systemd's
# user manager is reachable through the standard environment.
- name: Ensure the Tandoor data directories exist
ansible.builtin.file:
path: "{{ host_directory }}/{{ item }}"
@@ -13,16 +7,12 @@
group: "{{ podman_user }}"
mode: '0755'
loop:
# mediafiles is the irreplaceable one; the other two are regenerated by the
# app and are bind-mounted anyway because the image expects them to exist.
# mediafiles is irreplaceable. The other two are regenerated and bind-mounted because the image expects them.
- mediafiles
- staticfiles
- externalfiles
# The app's secrets and database coordinates are consumed by systemd
# (EnvironmentFile= in the Quadlet unit), not by Ansible, so this has to be
# written before the unit starts. Mode 0600: it holds SECRET_KEY and the
# postgres password.
# systemd reads it through EnvironmentFile= before the unit starts, and it holds SECRET_KEY and the Postgres password.
- name: Write the environment file
ansible.builtin.template:
src: tandoor.env.j2
@@ -52,9 +42,7 @@
state: started
daemon_reload: true
rescue:
# systemd reports "control process exited with error code" and nothing
# else, which is useless for the usual failures (an unattached network, an
# unreachable database, a bad volume). Pull the real reason out.
# systemd only reports "control process exited with error code", so pull the real reason.
- name: Collect Tandoor diagnostics
ansible.builtin.command: "{{ item }}"
loop:
@@ -73,16 +61,14 @@
ansible.builtin.fail:
msg: "Tandoor did not start — see the diagnostics above."
# Quadlet honours [Install] WantedBy= when it generates the unit, so
# `systemctl --user enable` is neither needed nor possible on a generated unit.
# Quadlet honours [Install] WantedBy=, so `systemctl --user enable` is not needed on a generated unit.
- name: Verify Tandoor is running
ansible.builtin.command: "systemctl --user is-active {{ unit_name }}"
register: tandoor_active
changed_when: false
failed_when: tandoor_active.stdout != 'active'
# `db` is the alias the environment file actually asks for; the container name
# is checked too because it is what the Quadlet network gives us for free.
# The environment file asks for `db`; the container name is checked too because the network provides it.
- name: Confirm Tandoor can resolve its database
ansible.builtin.command: >-
podman exec {{ container_name }} getent hosts {{ item }}
@@ -100,15 +86,8 @@
{{ item.stdout | default('(no answer)') }}
loop: "{{ tandoor_db_lookup.results }}"
# `is-active` says the container is running; it does not say the app can serve.
# Tandoor's entrypoint runs Django migrations before gunicorn answers, and until
# it does the image's own nginx returns 502 to Caddy — which Caddy relays as-is,
# complete with `server: nginx`. That is exactly the confusing "nginx 502" a
# first deploy shows after a play that reported success.
#
# python3 is used rather than curl or wget because this is a Django image: the
# interpreter is guaranteed present, the others are not. A 502 raises HTTPError,
# so the retry loop holds until a real page comes back.
# `is-active` only says the container is running. Tandoor migrates before gunicorn answers, and its nginx returns 502 until then.
# The probe uses python3 because the Django image guarantees it and may not ship curl or wget.
- name: Wait for Tandoor to serve HTTP
ansible.builtin.command: >-
podman exec {{ container_name }}
+5 -26
View File
@@ -1,50 +1,29 @@
---
# ---------------------------------------------------------------------------
# Rootless Podman / Quadlet
# ---------------------------------------------------------------------------
# Duplicated on purpose from caddy/vars/default.yml: a play cannot include that
# file wholesale, because it also sets `host_directory` — to the Caddy data
# directory — and every playbook defines that for itself. Keep these three in
# sync with caddy/vars/default.yml. (Its fourth, podman_unprivileged_port_start,
# belongs to the podman tasks, which this play does not run: Tandoor listens on
# 80 inside the container and publishes nothing host-side.)
#
# podman_user is derived from the connection: this play carries no become, so
# the Quadlet units belong to whoever Ansible connects as. Requires gather_facts
# (set in playbook.yml).
# Duplicated from caddy/vars/default.yml on purpose, so keep them in sync.
# podman_user comes from the connection user, so the play needs gather_facts.
podman_user: "{{ ansible_facts['user_id'] }}"
podman_data_dir: "{{ ansible_facts['user_dir'] }}/podman-data"
quadlet_dir: "{{ ansible_facts['user_dir'] }}/.config/containers/systemd"
# ---------------------------------------------------------------------------
# Tandoor
# ---------------------------------------------------------------------------
host: recipes.tanshu.com
container_name: tandoor
unit_name: "{{ container_name }}.service"
# Fully qualified on purpose: rootless Podman resolves short names against
# unqualified-search-registries, and there is no TTY to answer a prompt when
# the systemd unit starts.
# Fully qualified because rootless Podman would otherwise prompt for a registry, and a unit has no TTY.
image: docker.io/vabene1111/recipes:latest
# The Quadlet file is <network_name>.network. caddy/roles/register pins the
# Podman network name to <network_name>_net (so: tandoor.network, tandoor_net).
# caddy/roles/register pins the Podman network to <network_name>_net, so the Quadlet file is tandoor.network.
network_name: "{{ container_name }}"
host_directory: "{{ podman_data_dir }}/{{ container_name }}"
# Port Caddy proxies to, inside the container (the image's own nginx).
# The port Caddy proxies to inside the container, where the image's own nginx listens.
container_port: 80
# The shared Postgres unit this app attaches itself to (postgres/playbook.yml).
# roles/postgres owns this app's drop-ins there and the "Restart Postgres"
# handler that applies them.
postgres_container: postgres
postgres_unit: "{{ postgres_container }}.service"
# The shared Caddy unit and its Caddyfile, both owned by caddy/playbook.yml.
caddy_name: caddy
caddyfile_path: "{{ podman_data_dir }}/{{ caddy_name }}/conf/Caddyfile"