mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-11 13:17:07 +00:00
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>
157 lines
5.5 KiB
Python
157 lines
5.5 KiB
Python
import importlib.util
|
|
import unittest
|
|
import unittest.mock as mock
|
|
|
|
from ansible.parsing.dataloader import DataLoader
|
|
from ansible.template import Templar
|
|
|
|
from utils.storage.nfs import client_src, state_path
|
|
|
|
|
|
def _run(name, variables):
|
|
"""Lookups must read resolved values from `variables`; templar.template('{{..}}')
|
|
no-ops on untrusted strings in ansible 2.19+ and emits literal Jinja into exports."""
|
|
spec = importlib.util.spec_from_file_location(name, f"plugins/lookup/{name}.py")
|
|
m = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(m)
|
|
lm = m.LookupModule()
|
|
lm._templar = Templar(loader=DataLoader(), variables=variables)
|
|
return lm.run([], variables=variables)[0]
|
|
|
|
|
|
BASE = {
|
|
"storage": {"nfs": {"server": "192.168.244.2"}},
|
|
"RUNTIME": "github",
|
|
}
|
|
|
|
|
|
class TestNfsLookups(unittest.TestCase):
|
|
def test_state_path_renders(self):
|
|
self.assertEqual(_run("nfs_state_path", BASE), "/srv/nfs/infinito-state")
|
|
|
|
def test_fstype_renders(self):
|
|
self.assertEqual(_run("nfs_fstype", BASE), "nfs4")
|
|
|
|
def test_mount_opts_github_soft(self):
|
|
self.assertEqual(
|
|
_run("nfs_mount_opts", BASE),
|
|
"vers=4,rw,noresvport,soft,timeo=50,retrans=3",
|
|
)
|
|
|
|
def test_mount_opts_host_hard(self):
|
|
self.assertEqual(
|
|
_run("nfs_mount_opts", {**BASE, "RUNTIME": "host"}),
|
|
"vers=4,rw,noresvport,hard,timeo=600",
|
|
)
|
|
|
|
def test_mount_opts_act_soft(self):
|
|
self.assertEqual(
|
|
_run("nfs_mount_opts", {**BASE, "RUNTIME": "act"}),
|
|
"vers=4,rw,noresvport,soft,timeo=50,retrans=3",
|
|
)
|
|
|
|
|
|
def _load(name, variables):
|
|
spec = importlib.util.spec_from_file_location(name, f"plugins/lookup/{name}.py")
|
|
m = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(m)
|
|
lm = m.LookupModule()
|
|
lm._templar = Templar(loader=DataLoader(), variables=variables)
|
|
lm._loader = DataLoader()
|
|
return m, lm
|
|
|
|
|
|
class TestNfsConfigBackedLookups(unittest.TestCase):
|
|
"""nfs_flavor / nfs_client_src reach the merged-config SPOT through
|
|
lookup('config') via lookup_loader, NOT a utils.cache import."""
|
|
|
|
def test_flavor_returns_config_lookup_result(self):
|
|
m, lm = _load("nfs_flavor", {})
|
|
with mock.patch.object(m, "lookup_loader") as loader:
|
|
loader.get.return_value = mock.MagicMock(run=lambda *a, **k: ["ganesha"])
|
|
self.assertEqual(lm.run([], variables={}), ["ganesha"])
|
|
|
|
def test_flavor_config_call_shape(self):
|
|
m, lm = _load("nfs_flavor", {})
|
|
calls = []
|
|
|
|
def _run(args, **kwargs):
|
|
calls.append((tuple(args), kwargs))
|
|
return ["kernel"]
|
|
|
|
with mock.patch.object(m, "lookup_loader") as loader:
|
|
loader.get.return_value = mock.MagicMock(run=_run)
|
|
lm.run([], variables={"RUNTIME": "github"})
|
|
(args, kwargs) = calls[0]
|
|
self.assertEqual(
|
|
args,
|
|
("svc-storage-nfs-server", "services.nfs-server.flavor", "kernel"),
|
|
)
|
|
self.assertEqual(kwargs.get("variables"), {"RUNTIME": "github"})
|
|
|
|
def test_client_src_kernel_v4_uses_root_mount(self):
|
|
nfs = {"server": "10.0.0.2"}
|
|
vars_ = {"storage": {"nfs": nfs}}
|
|
m, lm = _load("nfs_client_src", vars_)
|
|
with mock.patch.object(m, "lookup_loader") as loader:
|
|
loader.get.return_value = mock.MagicMock(run=lambda *a, **k: ["kernel"])
|
|
result = lm.run([], variables=vars_)
|
|
expected = client_src(
|
|
"10.0.0.2",
|
|
4,
|
|
"kernel",
|
|
state_path("/srv/nfs", "infinito-state"),
|
|
)
|
|
self.assertEqual(result, [expected])
|
|
|
|
|
|
class TestNfsTmpfsMountpointsLookup(unittest.TestCase):
|
|
def test_only_nfs_backed_volumes_yield_octal_string_modes(self):
|
|
vars_ = {"application_id": "web-app-demo"}
|
|
applications = {
|
|
"web-app-demo": {
|
|
"volumes": {
|
|
"cache": {
|
|
"type": "volume",
|
|
"name": "demo_cache",
|
|
"mounts": [{"service": "app", "target": "/app/cache"}],
|
|
},
|
|
"local": {
|
|
"type": "volume",
|
|
"mounts": [{"service": "app", "target": "/app/local"}],
|
|
},
|
|
"static": {
|
|
"type": "tmpfs",
|
|
"mounts": [
|
|
{
|
|
"service": "app",
|
|
"target": "/app/cache/static",
|
|
"mode": "1777",
|
|
}
|
|
],
|
|
},
|
|
"scratch": {
|
|
"type": "tmpfs",
|
|
"mounts": [{"service": "app", "target": "/app/local/tmp"}],
|
|
},
|
|
}
|
|
}
|
|
}
|
|
compose_volumes = {
|
|
"cache": {
|
|
"name": "demo_cache",
|
|
"driver": "local",
|
|
"driver_opts": {"type": "none", "o": "bind"},
|
|
},
|
|
"local": {},
|
|
}
|
|
m, lm = _load("nfs_tmpfs_mountpoints", vars_)
|
|
with mock.patch.object(m, "lookup_loader") as loader:
|
|
loader.get.return_value = mock.MagicMock(run=lambda *a, **k: [applications])
|
|
result = lm.run([], variables=vars_, volumes=compose_volumes)
|
|
self.assertEqual(result, [[{"path": "demo_cache/static", "mode": "01777"}]])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|