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

+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 }}"
+11 -87
View File
@@ -1,33 +1,5 @@
---
# Gitea 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.
# The data tree is created here rather than left to the image, and that is not
# housekeeping. With keep-id:uid={{ gitea_uid }} the container process runs as
# `git`, so /usr/bin/entrypoint is the *only* part of the image's start-up that
# root can do — and it needs root for /etc/passwd, /etc/s6 and this tree.
#
# Container root is a subuid on the host, but inside its user namespace it holds
# the usual capabilities, so `mkdir`, `chmod` and `chown` all succeed; what it
# creates is simply left owned by that subuid. The image chowns the directories it
# knows about back to `git` — `chown -R ${USER}:git /data/gitea` and `/data/git`
# in /etc/s6/gitea/setup, which is where app.ini comes from — so those work either
# way. The things it does *not* chown are what this task is for, and they are the
# ones that would otherwise be subuid-owned and invisible to the app and to a
# plain tar backup:
#
# gitea/ app.ini, gitea.db, sessions, avatars, indexers
# gitea/conf where the entrypoint would generate app.ini
# gitea/log Gitea's file log
# git/ repositories, LFS, and the git account's HOME
# git/.ssh authorized_keys — written by Gitea, read by the host's sshd
# ssh/ only ever a *mount point* — see below
#
# The sshd *inside* the image is the exception; it cannot use this tree at all.
# See {{ container_sshd_hostkeys }} below.
# Create the tree as podman_user: anything left to the image stays subuid-owned and misses a plain tar backup.
- name: Ensure the Gitea data directory exists
ansible.builtin.file:
path: "{{ host_directory }}"
@@ -48,27 +20,12 @@
- { path: gitea/conf, mode: '0755' }
- { path: gitea/log, mode: '0755' }
- { path: git, mode: '0755' }
# 0700 because that is what the image's own setup script chmods it to
# (`chmod 700 /data/git/.ssh` in /etc/s6/gitea/setup, which runs as root
# before `su-exec git`). Set here so the mode never depends on that script
# having run.
# The image's setup chmods this to 0700; setting it here does not depend on that script.
- { path: git/.ssh, mode: '0700' }
# The in-image sshd's host keys are mounted over /data/ssh from
# {{ container_sshd_hostkeys }}, so this directory is never written to — it
# has to *exist*, because it is the mount point. If it does not, Podman
# creates it during container set-up, inside the container's user namespace,
# and it lands here owned by the subuid rather than by {{ podman_user }}:
#
# drwxr-xr-t 2 165536 165536 4096 ssh
#
# which the check at the end of this role then (correctly) fails on. Creating
# it first is what keeps the data tree wholly {{ podman_user }}'s.
# This is a mount point, so it must exist; letting Podman create it leaves it subuid-owned and fails the owner check.
- { path: ssh, mode: '0755' }
# The image's setup creates this file if it is missing, as root — and it is the
# one file under /data the setup never chowns back to `git`, so on a fresh host it
# would be left subuid-owned. Writing it here, as {{ podman_user }} and 0600,
# both avoids that and states the value the image's own environment carries.
# The image would create this as root and never chown it back, leaving it subuid-owned.
- name: Write the container-side .ssh/environment file
ansible.builtin.copy:
dest: "{{ host_directory }}/git/.ssh/environment"
@@ -78,16 +35,7 @@
group: "{{ podman_user }}"
mode: '0600'
# The image always starts an sshd of its own inside the container, and that sshd
# wants host keys. This deployment does not use it — git-over-SSH arrives on the
# host's sshd on port 22 — but it has to have somewhere to write, and the process
# that runs it maps to a subuid that cannot touch the data directory. Keeping it
# out here means no subuid-owned file ever lands in {{ host_directory }}, so a
# plain tar/rsync of the data tree stays complete and error-free, and the check
# at the end of this role stays meaningful.
#
# 0777 is deliberate and bounded: /home/{{ podman_user }} is 0750, so no other
# host user can reach this path at all. The mode only matters inside the userns.
# The in-image sshd runs as a subuid that cannot touch the data directory, and 0777 is safe because the home directory is 0750.
- name: Ensure the scratch host key directory exists
ansible.builtin.file:
path: "{{ container_sshd_hostkeys }}"
@@ -107,16 +55,7 @@
- Reload systemd user units
- Restart Gitea
# Quadlet honours [Install] WantedBy= when it generates the unit, so
# `systemctl --user enable` is neither needed nor possible on a generated unit.
#
# All three steps are in one block because they fail the same way and want the
# same evidence. `systemctl start` reports success as soon as it has handed the
# unit to systemd, so a container that dies a second later — a `User=0` missing,
# an unwritable /data, a subuid that cannot run the image's start-up — surfaces
# only as `activating` from `is-active`, or as a health check that never passes,
# with the reason in the journal and nowhere else. Restart=always makes that a
# crash loop rather than an error.
# Quadlet honours [Install] WantedBy=, so the generated unit is never enabled.
- name: Start and verify Gitea
block:
- name: Start Gitea
@@ -126,9 +65,7 @@
state: started
daemon_reload: true
# Retried rather than checked once: podman reports `activating` for the
# interval between systemd starting the container and its readiness
# notification.
# Retried because podman reports `activating` between start and readiness.
- name: Verify Gitea is running
ansible.builtin.command: "systemctl --user is-active {{ unit_name }}"
register: gitea_active
@@ -137,9 +74,7 @@
delay: 5
changed_when: false
# `is-active` says the container is running; it does not say the app can
# serve. The image ships no HEALTHCHECK, so the unit defines one (HealthCmd=,
# see the template) and this runs exactly that command once, synchronously.
# The image ships no HEALTHCHECK, so the unit defines one and this runs it synchronously.
- name: Wait for Gitea to pass its health check
ansible.builtin.command: "podman healthcheck run {{ container_name }}"
register: gitea_health
@@ -148,10 +83,7 @@
delay: 5
changed_when: false
rescue:
# systemd reports "control process exited with error code" and nothing else,
# which is useless for the usual failures. The journal is where the image's
# own start-up output is — that is where a s6 or entrypoint permission
# failure shows up.
# systemd only reports "control process exited with error code", so read the journal for the real reason.
- name: Collect Gitea diagnostics
ansible.builtin.command: "{{ item }}"
loop:
@@ -170,13 +102,7 @@
ansible.builtin.fail:
msg: "Gitea did not start — see the diagnostics above."
# A wrong keep-id is the one failure here that does not look like one: Gitea
# still serves, but from files it owns as an unmapped subuid on the host — files
# that then disappear from a tar backup and cannot be deleted without
# `podman unshare`. Check the whole tree, not a sample.
#
# Deliberately not `failed_when: false`: if `find` cannot read something in the
# data directory, the tree is not provably clean, and that is the finding.
# A wrong keep-id still serves, but writes subuid-owned files a tar backup misses, so check the whole tree and let find fail.
- name: List anything in the data tree not owned by {{ podman_user }}
ansible.builtin.command: >-
find {{ host_directory }} ! -user {{ podman_user }}
@@ -198,9 +124,7 @@
path: "{{ host_directory }}/gitea/gitea.db"
register: gitea_db
# Not a failure: a fresh instance legitimately has no database yet. But on a
# migration it means the copy in readme.md has not run, and the instance that is
# now serving is an empty installer rather than this repository's Gitea.
# Not a failure on a fresh instance, but a migration whose copy never ran will serve an empty installer.
- name: Report a data directory with no database in it
ansible.builtin.debug:
msg: >-
+5 -49
View File
@@ -1,44 +1,11 @@
---
# The host half of Gitea's git-over-SSH support.
#
# Port 22 belongs to the host's sshd and stays that way: clients keep using
# git@git.tanshu.com:user/repo.git, and no remote URL changes. Four host-side
# pieces make that work:
#
# 1. a `{{ git_user }}` login user with no interactive access — every key
# Gitea accepts carries a forced command, so sshd runs the shim and
# nothing else;
# 2. {{ shim_path }}, the shim those forced commands invoke;
# 3. {{ podman_exec_wrapper }} plus a one-command sudoers rule — how the shim,
# running as `{{ git_user }}`, reaches a container owned by
# {{ podman_user }};
# 4. the sshd and sudoers drop-ins that tie the above together.
#
# What differs from the Docker-era role is the second hop. It used to be an ssh
# into an sshd running *inside* the container; under keep-id that sshd runs as a
# subuid and cannot read the authorized_keys Gitea writes as {{ podman_user }},
# so the hop is a `podman exec` instead. readme.md, "How git over SSH works
# now", is where the reasoning lives.
#
# Every task here is root-only (users, /usr/local/bin, /etc/ssh, /etc/sudoers),
# so each elevates on its own. The play carries no become, because the Quadlet
# units have to belong to the connection user.
# Git-over-SSH uses the host's sshd on port 22, and the shim reaches the container through a one-command sudoers rule.
- name: Ensure the git group exists
ansible.builtin.group:
name: "{{ git_user }}"
become: true
# The shell matters even though nobody ever gets one: sshd runs a forced command
# through the login user's shell (`/bin/bash -c '...'`), so a nologin shell would
# break git-over-SSH outright.
#
# The uid is deliberately left to the system. On monoco the host git account was
# uid 1003 because the container's git account was 1003 and the two shared one
# uid space; under keep-id the container's 1003 is mapped onto {{ podman_user }}
# instead, so nothing on this host depends on this number any more — not the
# key file (owned by name), not the sudoers rule (matched by name), not the
# authorized_keys hook (run as {{ podman_user }}).
# sshd runs the forced command through the login user's shell, so it must not be nologin; the uid is left to the system.
- name: Add the git login user
ansible.builtin.user:
name: "{{ git_user }}"
@@ -49,11 +16,6 @@
create_home: true
become: true
# There is deliberately no ~/.ssh for this user any more. Its old contents were
# the shim's client key and the authorized_keys Gitea maintains; the key is
# obsolete (nothing authenticates over ssh in this design) and the file lives in
# the container's data tree now, where Gitea writes it. The migration notes in
# readme.md move the file into place.
- name: Install the git-over-SSH passthrough shim
ansible.builtin.template:
@@ -82,9 +44,7 @@
mode: '0755'
become: true
# valid with `visudo -cf %s`: a syntax error in /etc/sudoers.d does not fail one
# rule, it fails *every* sudo invocation on the host, which on a server reached
# by ssh is a nasty way to find out.
# A syntax error in /etc/sudoers.d breaks every sudo on the host, so validate before installing.
- name: Let the git user reach the container as {{ podman_user }}
ansible.builtin.template:
src: sudoers-gitea.j2
@@ -107,10 +67,7 @@
register: gitea_sshd_dropin
become: true
# A drop-in that does not parse is not a failed play, it is a host nobody can
# log in to after the next reboot: sshd reads the whole directory at start-up.
# Validate the resulting configuration before it is ever loaded, and take the
# file away again if it does not parse.
# A drop-in that does not parse locks everyone out on the next reboot, so validate it and remove the file if it fails.
- name: Validate the resulting sshd configuration
ansible.builtin.command: /usr/sbin/sshd -t
changed_when: false
@@ -130,8 +87,7 @@
sshd is still running — and still restarting — on its previous
configuration. Run `/usr/sbin/sshd -t` on the host to see why.
# `reload`, not `restart`: a reload re-execs sshd without dropping the connection
# this play is running over.
# A reload re-execs sshd without dropping the connection this play runs over.
- name: Reload sshd
ansible.builtin.systemd_service:
name: "{{ sshd_service }}"