Add the Yamtrack playbook
This commit is contained in:
1 parent
26a0654fea
commit
af2e7394b8
11 files changed
+1023
No files matched your search
@@ -0,0 +1,31 @@
|
||||
---
|
||||
# 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.
|
||||
- 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.
|
||||
- name: Restart Caddy
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ caddy_name }}.service"
|
||||
scope: user
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
listen: Restart Caddy
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
# 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. (The Docker-era version of this role did exactly that.)
|
||||
|
||||
- name: "Add the {{ host }} site block to the Caddyfile"
|
||||
ansible.builtin.blockinfile:
|
||||
path: "{{ caddyfile_path }}"
|
||||
marker: "# {mark} Ansible managed Caddy snippet for {{ host }}"
|
||||
block: "{{ snippet_block }}"
|
||||
create: yes
|
||||
owner: "{{ podman_user }}"
|
||||
group: "{{ podman_user }}"
|
||||
mode: '0644'
|
||||
notify: Reload Caddy
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
# Valkey is its own playbook (valkey/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 valkey/roles/attach, which notifies it whenever this app's drop-in
|
||||
# changes: a new drop-in changes the generated unit, so Valkey must be recreated
|
||||
# to join the network — Quadlet has no live equivalent of
|
||||
# `podman network connect`.
|
||||
#
|
||||
# The cost is a cold cache for every attached app, not lost data — but note that
|
||||
# attached means every cache client, not just Yamtrack. See valkey/readme.md,
|
||||
# "The trade-off".
|
||||
- name: Restart Valkey
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ valkey_unit }}"
|
||||
scope: user
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
listen: Restart Valkey
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
# Attach this app's network to the shared Valkey unit.
|
||||
#
|
||||
# Valkey knows nothing about its clients, exactly like Caddy and Postgres: a
|
||||
# service joins by dropping 50-yamtrack.conf and the shared
|
||||
# 10-network-aliases.conf next to valkey.container. The canonical way to do that
|
||||
# is the shared include below — it writes the same bytes every service writes,
|
||||
# asserts that yamtrack.network already exists, and notifies the "Restart Valkey"
|
||||
# handler defined in this role.
|
||||
#
|
||||
# `valkey` is the host REDIS_URL asks for — the container name, not the shared
|
||||
# `redis` alias, which is there for the clients whose config.php says `redis`.
|
||||
# The Docker-era container was given `redis://valkey:6379` too, so the URL is
|
||||
# unchanged.
|
||||
#
|
||||
# This runs in the roles phase, which is after the register include in
|
||||
# playbook.yml's pre_tasks has created yamtrack.network.
|
||||
|
||||
- name: Attach Yamtrack to Valkey
|
||||
ansible.builtin.include_tasks: "{{ playbook_dir }}/../valkey/roles/attach/tasks/main.yaml"
|
||||
|
||||
# On a first attach the drop-in is new, so Valkey has to be recreated to join
|
||||
# yamtrack_net — and that restart is a *handler*, which by default runs at the
|
||||
# end of the play, after the yamtrack role has already started the app. The app
|
||||
# would then come up before `valkey` resolves, fail its own checks, 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-in is unchanged, nothing is pending, and this is a no-op.
|
||||
- name: Apply the Valkey network change before starting Yamtrack
|
||||
ansible.builtin.meta: flush_handlers
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
# All handlers run as the connection user ({{ podman_user }}), so systemctl
|
||||
# --user works without setting XDG_RUNTIME_DIR.
|
||||
|
||||
- name: Reload systemd user units
|
||||
ansible.builtin.command: systemctl --user daemon-reload
|
||||
changed_when: true
|
||||
listen: Reload systemd user units
|
||||
|
||||
# A changed environment file or unit file changes the generated unit, so
|
||||
# Yamtrack is recreated. The database is a bind mount and the rest of /yamtrack
|
||||
# is image content, so a recreate loses nothing — but it is a few seconds of
|
||||
# downtime for the one site.
|
||||
- name: Restart Yamtrack
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ unit_name }}"
|
||||
scope: user
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
listen: Restart Yamtrack
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
# Yamtrack 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.
|
||||
|
||||
# /yamtrack/db is the image's state directory: db.sqlite3 plus its -wal/-shm
|
||||
# sidecars, and nothing else worth keeping — staticfiles/ is built into the
|
||||
# image and the code is read-only.
|
||||
- name: Ensure the Yamtrack data directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ data_dir }}"
|
||||
state: directory
|
||||
owner: "{{ podman_user }}"
|
||||
group: "{{ podman_user }}"
|
||||
mode: '0755'
|
||||
|
||||
# The app's secrets and cache 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.
|
||||
- name: Write the environment file
|
||||
ansible.builtin.template:
|
||||
src: yamtrack.env.j2
|
||||
dest: "{{ env_file }}"
|
||||
owner: "{{ podman_user }}"
|
||||
group: "{{ podman_user }}"
|
||||
mode: '0600'
|
||||
notify: Restart Yamtrack
|
||||
|
||||
- name: Install the Yamtrack Quadlet unit
|
||||
ansible.builtin.template:
|
||||
src: yamtrack.container.j2
|
||||
dest: "{{ quadlet_dir }}/{{ container_name }}.container"
|
||||
owner: "{{ podman_user }}"
|
||||
group: "{{ podman_user }}"
|
||||
mode: '0644'
|
||||
notify:
|
||||
- Reload systemd user units
|
||||
- Restart Yamtrack
|
||||
|
||||
- name: Start Yamtrack
|
||||
block:
|
||||
- name: Start Yamtrack
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ unit_name }}"
|
||||
scope: user
|
||||
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 cache, a bad volume). Pull the real reason out.
|
||||
- name: Collect Yamtrack diagnostics
|
||||
ansible.builtin.command: "{{ item }}"
|
||||
loop:
|
||||
- "systemctl --user status {{ unit_name }} --no-pager"
|
||||
- "journalctl --user -u {{ unit_name }} --no-pager -n 40"
|
||||
- "podman logs --tail 40 {{ container_name }}"
|
||||
register: yamtrack_diag
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Show Yamtrack diagnostics
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ yamtrack_diag.results | map(attribute='stdout') | list }}"
|
||||
|
||||
- name: Fail
|
||||
ansible.builtin.fail:
|
||||
msg: "Yamtrack 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.
|
||||
#
|
||||
# `is-active` says the container is running; it does not say the app can serve.
|
||||
# The entrypoint runs migrations before supervisord starts nginx, gunicorn,
|
||||
# celery and beat, and `Restart=always` means even a crash-looping container is
|
||||
# reported active — so the checks below are the ones that matter.
|
||||
- name: Verify Yamtrack is running
|
||||
ansible.builtin.command: "systemctl --user is-active {{ unit_name }}"
|
||||
register: yamtrack_active
|
||||
changed_when: false
|
||||
failed_when: yamtrack_active.stdout != 'active'
|
||||
|
||||
# `valkey` is the host the environment file actually asks for; it resolves from
|
||||
# ContainerName= on the shared unit, which valkey/roles/attach joined to this
|
||||
# app's network.
|
||||
- name: Confirm Yamtrack can resolve its cache
|
||||
ansible.builtin.command: >-
|
||||
podman exec {{ container_name }} getent hosts {{ valkey_container }}
|
||||
register: yamtrack_cache_lookup
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Report cache resolution
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ valkey_container }} resolves as
|
||||
{{ yamtrack_cache_lookup.stdout | default('(no answer)') }}
|
||||
|
||||
# `is-active` says the container is running; the image's own HEALTHCHECK is what
|
||||
# says the app works. It wgets http://127.0.0.1:8000/health/, and that view
|
||||
# checks all four things this deployment can get wrong — the cache, the database,
|
||||
# Redis and Celery — so a green result here is the app being genuinely ready.
|
||||
#
|
||||
# Running the image's check rather than one written here means depending on
|
||||
# nothing the play has to know: no curl or wget wrapper of our own, and no
|
||||
# hardcoded path that a later image tag could move. `podman exec <c> wget -qO-
|
||||
# http://127.0.0.1:{{ container_port }}/health/` prints the per-check breakdown
|
||||
# when this fails; see readme.md, "Verification".
|
||||
#
|
||||
# The start period (30s) plus the entrypoint's migrations are why this allows
|
||||
# three minutes rather than one.
|
||||
- name: Wait for Yamtrack to pass its own health check
|
||||
ansible.builtin.command: "podman healthcheck run {{ container_name }}"
|
||||
register: yamtrack_health
|
||||
until: yamtrack_health.rc == 0
|
||||
retries: 36
|
||||
delay: 5
|
||||
changed_when: false
|
||||
failed_when: yamtrack_health.rc != 0
|
||||
@@ -0,0 +1,56 @@
|
||||
# Yamtrack — rootless Podman Quadlet unit.
|
||||
# Managed by Ansible (yamtrack/roles/yamtrack). Do not edit by hand.
|
||||
#
|
||||
# This unit joins {{ network_name }}.network, which is created by
|
||||
# caddy/roles/register (the same include that attaches Caddy to it). The shared
|
||||
# valkey container is joined to that network by valkey/roles/attach, which is
|
||||
# what makes the host in REDIS_URL resolve.
|
||||
|
||||
[Unit]
|
||||
Description=Yamtrack
|
||||
Documentation=https://github.com/FuzzyGrim/Yamtrack
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Container]
|
||||
ContainerName={{ container_name }}
|
||||
Image={{ image }}
|
||||
|
||||
# keep-id maps *container root* onto {{ podman_user }} on the host, so
|
||||
# {{ data_dir }} stays owned by {{ podman_user }} and never by a subuid, and a
|
||||
# plain tar backup keeps working without `podman unshare` or `sudo`.
|
||||
#
|
||||
# uid 0 is not incidental. `--userns=keep-id` also decides which user the
|
||||
# container runs as — the *mapped* one — and the image's entrypoint needs real
|
||||
# root for its first steps: `groupmod` and `usermod` rewrite /etc/group and
|
||||
# /etc/passwd, which are owned by container-root in the image. With
|
||||
# `keep-id:uid=1000` the entrypoint runs as `abc`, cannot lock /etc/group, and
|
||||
# exits 10 before supervisord is ever reached. The entrypoint's PUID/PGID are
|
||||
# pinned to the same 0 in the environment file, so `abc` resolves to container
|
||||
# root and everything the app writes lands on {{ podman_user }}.
|
||||
#
|
||||
# See readme.md, "Who runs as whom".
|
||||
UserNS=keep-id:uid={{ app_uid }},gid={{ app_gid }}
|
||||
|
||||
# SECRET, TZ, URLS and REDIS_URL live here rather than as Environment= lines
|
||||
# above: this unit is world-readable in ~/.config, and SECRET signs sessions and
|
||||
# password-reset tokens. Read by systemd, not by Ansible, so the file has to
|
||||
# exist before this unit starts.
|
||||
EnvironmentFile={{ env_file }}
|
||||
|
||||
Network={{ network_name }}.network
|
||||
|
||||
# The image's state directory: db.sqlite3 and its -wal/-shm sidecars. The rest
|
||||
# of /yamtrack is image content (staticfiles/ is built in, the code is
|
||||
# read-only), so this is the whole of what has to survive.
|
||||
Volume={{ data_dir }}:/yamtrack/db
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
# The entrypoint runs `manage.py migrate --noinput` before supervisord starts
|
||||
# nginx, gunicorn, celery and celery beat, so allow for a slow first start on a
|
||||
# large database.
|
||||
TimeoutStartSec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,37 @@
|
||||
# Environment for {{ container_name }}.container.
|
||||
# Managed by Ansible (yamtrack/roles/yamtrack). Do not edit by hand.
|
||||
#
|
||||
# Mode 0600 on purpose: the Quadlet unit that reads this file is world-readable,
|
||||
# and SECRET signs sessions and password-reset tokens. Note that `podman inspect`
|
||||
# still prints the resulting environment to anyone who can run podman as this
|
||||
# user.
|
||||
#
|
||||
# The first four are the variables the Docker-era container was given, so this
|
||||
# deployment behaves identically. ALLOWED_HOSTS is deliberately absent: its
|
||||
# default is `*`, which is what the old container relied on. PUID/PGID are new —
|
||||
# the Docker-era container ran as real root, and under rootless keep-id the
|
||||
# entrypoint needs to be told which uid is the mapped one. See readme.md, "Who
|
||||
# runs as whom".
|
||||
|
||||
# The Django SECRET_KEY. Carried over verbatim so existing sessions stay valid.
|
||||
SECRET={{ yamtrack_secret }}
|
||||
|
||||
# TIME_ZONE for the app and for celery beat's schedules.
|
||||
TZ={{ yamtrack_tz }}
|
||||
|
||||
# CSRF trusted origins. The old container set `URLS=https://{{ host }}`.
|
||||
URLS={{ urls }}
|
||||
|
||||
# The shared Valkey unit, attached to this app's network by
|
||||
# yamtrack/roles/valkey. Yamtrack requires Redis: it is the cache backend and
|
||||
# the Celery broker, with no fallback.
|
||||
REDIS_URL={{ redis_url }}
|
||||
|
||||
# The entrypoint runs `groupmod`/`usermod` to make the image's `abc` account
|
||||
# match these, then chowns the data directory to it. Its own defaults are 1000,
|
||||
# which under this unit's keep-id:uid=0 is a *subuid* on the host — the database
|
||||
# would come out owned by e.g. 165536, invisible to a plain tar backup. Pinning
|
||||
# both to 0 makes `abc` resolve to container root, which keep-id maps onto
|
||||
# podman_user.
|
||||
PUID={{ app_uid }}
|
||||
PGID={{ app_gid }}
|
||||
Reference in new issue
Block a user