Files
computer-playbook/plugins/lookup/nfs_tmpfs_mountpoints.py
T
kevinveenbirkenbachandClaude Opus 5.5 ae40a573c2 fix(web-app-weblate): give the static tmpfs mountpoint its mode on swarm NFS volumes
CI run 40979252521 Weblate#1 (swarm) never converged: every task after
the first died in collectstatic with "PermissionError: [Errno 13]
Permission denied: '/app/cache/static/weblate_fonts'". The tmpfs at
/app/cache/static sits inside the shared weblate_cache volume; the first
task's runc creates the mountpoint as root 0755, and runc gives a tmpfs
on an existing mountpoint that directory's mode. 99a7d28067 declared
mode 1777, which compose honours, but docker stack deploy keeps only the
tmpfs size (docker/cli 29.9.2 convert/volume.go), so swarm still got a
root 0755 tmpfs.

- utils/storage/nfs.py: nested_tmpfs_mountpoints() derives every tmpfs
  mountpoint whose innermost enclosing volume of the same service is
  NFS-backed, with its declared mode; a nested tmpfs without a mode, or
  one mountpoint declared with two modes, raises.
- plugins/lookup/nfs_tmpfs_mountpoints.py feeds it the rendered compose
  volumes, so compose_volumes stays the source of which volumes live on
  NFS.
- sys-svc-compose nfs_prep.yml creates those mountpoints on the NFS
  server with the declared mode before every stack deploy, which also
  repairs a mountpoint an earlier deploy left at 0755.
- web-app-weblate: the cache volume declares nfs uid 1000, gid 0,
  mode 0770; a pre-created mountpoint makes the volume non-empty, so
  Docker no longer copies the image's /app/cache root in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 02:05:59 +01:00

69 lines
2.5 KiB
Python

from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from ansible.errors import AnsibleError
from ansible.plugins.loader import lookup_loader
from ansible.plugins.lookup import LookupBase
from utils.storage.nfs import nested_tmpfs_mountpoints, volume_dir
class LookupModule(LookupBase):
"""Tmpfs mountpoints in NFS volumes: {{ lookup('nfs_tmpfs_mountpoints', ...) }}."""
def run(
self,
terms: list[Any] | None,
variables: dict[str, Any] | None = None,
**kwargs: Any,
) -> list[list[dict[str, str]]]:
"""Return one list of ``{"path", "mode"}`` items, ``mode`` as an octal string.
Args:
terms: none.
variables: the templating context; ``application_id`` is read from it.
kwargs: ``volumes``, the rendered top-level compose ``volumes:`` mapping.
"""
if terms:
raise AnsibleError("nfs_tmpfs_mountpoints lookup expects no terms")
if "volumes" not in kwargs:
raise AnsibleError("nfs_tmpfs_mountpoints lookup requires 'volumes'")
volumes = kwargs["volumes"]
if not isinstance(volumes, Mapping):
raise AnsibleError(
"nfs_tmpfs_mountpoints: 'volumes' must be the compose volumes "
f"mapping, got {type(volumes).__name__}"
)
vars_ = variables or self._templar.available_variables
application_id = str(self._templar.template(vars_["application_id"])).strip()
applications = lookup_loader.get(
"applications", loader=self._loader, templar=self._templar
).run([], variables=vars_)[0]
if application_id not in applications:
raise AnsibleError(
f"nfs_tmpfs_mountpoints: unknown application_id '{application_id}'"
)
nfs_dirs = {
name: volume_dir(name, spec)
for name, spec in volumes.items()
if isinstance(spec, Mapping) and "driver_opts" in spec
}
try:
mountpoints = nested_tmpfs_mountpoints(
applications[application_id].get("volumes"), nfs_dirs
)
except (TypeError, ValueError) as exc:
raise AnsibleError(
f"nfs_tmpfs_mountpoints: {application_id}: {exc}"
) from exc
return [
[
{"path": mountpoint["path"], "mode": f"0{mountpoint['mode']:o}"}
for mountpoint in mountpoints
]
]