mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 12:47:04 +00:00
compose_volumes merged extra_volumes first and then skipped every meta/volumes.yml entry whose key was already present, so the meta nfs value was lost with it. Nextcloud injects talk_recording_tmp through nextcloud_extra_volumes without an nfs key, and 5728502a19 marked that volume nfs: false. On swarm with the NFS backend the stack therefore bound it to /var/lib/infinito/nextcloud_talk_recording_tmp, while nfs_prep.yml renders the same lookup without extras, saw nfs: false and never created the subdir; every node rejected the task with 'no such file or directory'. The meta nfs value now carries over onto the injected or derived spec unless that spec sets nfs itself, so both renders agree and the scratch volume stays node-local.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
517 lines
18 KiB
Python
517 lines
18 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
import unittest.mock as mock
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from ansible.errors import AnsibleFilterError
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
|
|
def _ensure_repo_root_on_syspath() -> None:
|
|
if str(PROJECT_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
|
|
_ensure_repo_root_on_syspath()
|
|
|
|
from plugins.lookup.compose_volumes import compose_volumes # noqa: E402
|
|
from utils.cache.yaml import load_yaml_str # noqa: E402
|
|
|
|
_DIR_VAR_LIB = os.environ["INFINITO_DIR_VAR_LIB"]
|
|
|
|
|
|
def _call(applications: Any, application_id: str, **kwargs: Any) -> str:
|
|
kwargs.setdefault("dir_var_lib", _DIR_VAR_LIB)
|
|
return compose_volumes(applications, application_id, **kwargs)
|
|
|
|
|
|
class TestComposeVolumes(unittest.TestCase):
|
|
def _parse_yaml(self, rendered: str) -> dict[str, Any]:
|
|
self.assertIsInstance(rendered, str)
|
|
data = load_yaml_str(rendered) if rendered.strip() else {}
|
|
self.assertIsInstance(data, dict)
|
|
self.assertIn("volumes", data)
|
|
self.assertIsInstance(data["volumes"], dict)
|
|
return data
|
|
|
|
def _base_apps(self) -> dict[str, Any]:
|
|
return {
|
|
"app": {
|
|
"services": {
|
|
"mariadb": {"enabled": False, "shared": False},
|
|
"redis": {"enabled": False},
|
|
"sso": {"enabled": False, "flavor": "oauth2"},
|
|
}
|
|
}
|
|
}
|
|
|
|
def test_none_applications_raises(self):
|
|
with self.assertRaises(AnsibleFilterError):
|
|
_call(None, "app") # type: ignore[arg-type]
|
|
|
|
def test_non_dict_applications_raises(self):
|
|
with self.assertRaises(AnsibleFilterError):
|
|
_call(["not-a-dict"], "app") # type: ignore[arg-type]
|
|
|
|
def test_empty_application_id_raises(self):
|
|
apps = self._base_apps()
|
|
with self.assertRaises(AnsibleFilterError):
|
|
_call(apps, "") # type: ignore[arg-type]
|
|
|
|
def test_unknown_application_id_raises(self):
|
|
apps = self._base_apps()
|
|
with self.assertRaises(AnsibleFilterError):
|
|
_call(apps, "missing-app")
|
|
|
|
def test_renders_volumes_key_even_when_empty(self):
|
|
apps = self._base_apps()
|
|
rendered = _call(apps, "app")
|
|
data = self._parse_yaml(rendered)
|
|
self.assertEqual(data["volumes"], {})
|
|
|
|
def test_database_enabled_not_shared_derives_database_volume(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["services"]["mariadb"]["enabled"] = True
|
|
apps["app"]["services"]["mariadb"]["shared"] = False
|
|
|
|
rendered = _call(apps, "app")
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertEqual(data["volumes"]["database"]["name"], "app_database")
|
|
|
|
def test_database_enabled_shared_true_does_not_add_database_volume(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["services"]["mariadb"]["enabled"] = True
|
|
apps["app"]["services"]["mariadb"]["shared"] = True
|
|
|
|
rendered = _call(apps, "app")
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertNotIn("database", data["volumes"])
|
|
|
|
def test_database_enabled_shared_null_treated_as_not_shared(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["services"]["mariadb"]["enabled"] = True
|
|
apps["app"]["services"]["mariadb"]["shared"] = None
|
|
|
|
rendered = _call(apps, "app")
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertIn("database", data["volumes"])
|
|
self.assertEqual(data["volumes"]["database"]["name"], "app_database")
|
|
|
|
def test_redis_enabled_adds_redis_volume(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["services"]["redis"]["enabled"] = True
|
|
|
|
rendered = _call(apps, "app")
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertIn("redis", data["volumes"])
|
|
self.assertEqual(data["volumes"]["redis"]["name"], "app_redis")
|
|
|
|
def test_sso_oauth2_flavor_enabled_adds_redis_volume_when_redis_disabled(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["services"]["redis"]["enabled"] = False
|
|
apps["app"]["services"]["sso"]["enabled"] = True
|
|
|
|
rendered = _call(apps, "app")
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertIn("redis", data["volumes"])
|
|
self.assertEqual(data["volumes"]["redis"]["name"], "app_redis")
|
|
|
|
def test_sso_oidc_flavor_does_not_add_redis_if_redis_disabled(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["services"]["redis"]["enabled"] = False
|
|
apps["app"]["services"]["sso"]["enabled"] = True
|
|
apps["app"]["services"]["sso"]["flavor"] = "oidc"
|
|
|
|
rendered = _call(apps, "app")
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertNotIn("redis", data["volumes"])
|
|
|
|
def test_sso_null_does_not_add_redis_if_redis_disabled(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["services"]["redis"]["enabled"] = False
|
|
apps["app"]["services"]["sso"]["enabled"] = None
|
|
|
|
rendered = _call(apps, "app")
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertNotIn("redis", data["volumes"])
|
|
|
|
def test_extra_volumes_are_added(self):
|
|
apps = self._base_apps()
|
|
|
|
rendered = _call(
|
|
apps,
|
|
"app",
|
|
extra_volumes={"data": {"name": "pg_data_vol"}},
|
|
)
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertIn("data", data["volumes"])
|
|
self.assertEqual(data["volumes"]["data"]["name"], "pg_data_vol")
|
|
|
|
def test_extra_volumes_override_auto(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["services"]["redis"]["enabled"] = True
|
|
|
|
rendered = _call(
|
|
apps,
|
|
"app",
|
|
extra_volumes={"redis": {"name": "custom_redis"}},
|
|
)
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertEqual(data["volumes"]["redis"]["name"], "custom_redis")
|
|
|
|
def test_extra_secret_without_name_gets_the_content_hashed_name(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
one, two = Path(tmp) / "one.yaml", Path(tmp) / "two.yaml"
|
|
one.write_text("as_token: one\n", encoding="utf-8")
|
|
two.write_text("as_token: two\n", encoding="utf-8")
|
|
secrets = self._parse_yaml(
|
|
_call(
|
|
self._base_apps(),
|
|
"app",
|
|
extra_secrets={
|
|
"cfg": {"file": str(one)},
|
|
"other": {"file": str(two)},
|
|
"named": {"name": "kept", "file": str(one)},
|
|
},
|
|
)
|
|
)["secrets"]
|
|
|
|
self.assertEqual(secrets["cfg"]["file"], str(one))
|
|
self.assertRegex(secrets["cfg"]["name"], r"^app_cfg_[0-9a-f]{8}$")
|
|
self.assertRegex(secrets["other"]["name"], r"^app_other_[0-9a-f]{8}$")
|
|
self.assertNotEqual(secrets["cfg"]["name"][-8:], secrets["other"]["name"][-8:])
|
|
self.assertEqual(secrets["named"]["name"], "kept")
|
|
|
|
def test_database_enabled_not_shared_shared_provider_name_used_when_present(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["services"]["mariadb"]["enabled"] = True
|
|
apps["app"]["services"]["mariadb"]["shared"] = True
|
|
apps["svc-db-mariadb"] = {"services": {"mariadb": {"name": "mariadb-central"}}}
|
|
|
|
rendered = _call(apps, "app")
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertNotIn("database", data["volumes"])
|
|
|
|
def test_database_simultaneous_postgres_and_mariadb_raises(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["services"]["mariadb"] = {"enabled": True, "shared": False}
|
|
apps["app"]["services"]["postgres"] = {"enabled": True, "shared": False}
|
|
with self.assertRaisesRegex(
|
|
AnsibleFilterError,
|
|
"Simultaneous postgres \\+ mariadb",
|
|
):
|
|
_call(apps, "app")
|
|
|
|
def test_seaweedfs_enabled_not_shared_adds_seaweedfs_volume(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["services"]["seaweedfs"] = {"enabled": True, "shared": False}
|
|
|
|
rendered = _call(apps, "app")
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertIn("seaweedfs", data["volumes"])
|
|
self.assertEqual(data["volumes"]["seaweedfs"]["name"], "app_seaweedfs")
|
|
|
|
def test_seaweedfs_enabled_shared_true_does_not_add_seaweedfs_volume(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["services"]["seaweedfs"] = {"enabled": True, "shared": True}
|
|
|
|
rendered = _call(apps, "app")
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertNotIn("seaweedfs", data["volumes"])
|
|
|
|
def test_minio_enabled_not_shared_adds_minio_volume(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["services"]["minio"] = {"enabled": True, "shared": False}
|
|
|
|
rendered = _call(apps, "app")
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertIn("minio", data["volumes"])
|
|
self.assertEqual(data["volumes"]["minio"]["name"], "app_minio")
|
|
|
|
def test_objstore_engines_disabled_add_no_volumes(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["services"]["seaweedfs"] = {"enabled": False, "shared": False}
|
|
apps["app"]["services"]["minio"] = {"enabled": False, "shared": False}
|
|
|
|
rendered = _call(apps, "app")
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertEqual(data["volumes"], {})
|
|
|
|
def test_extra_volume_with_none_name_serializes_to_null(self):
|
|
apps = self._base_apps()
|
|
|
|
rendered = _call(
|
|
apps,
|
|
"app",
|
|
extra_volumes={"data": {"name": None}},
|
|
)
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertIsNone(data["volumes"]["data"]["name"])
|
|
|
|
def test_swarm_nfs_opt_in_renders_driver_opts(self):
|
|
apps = self._base_apps()
|
|
rendered = _call(
|
|
apps,
|
|
"app",
|
|
extra_volumes={"images": {"name": "app_images", "nfs": True}},
|
|
deployment_mode="swarm",
|
|
storage={
|
|
"backend": "nfs",
|
|
"nfs": {"server": "10.0.0.20", "export_base": "/srv/nfs"},
|
|
},
|
|
)
|
|
data = self._parse_yaml(rendered)
|
|
|
|
vol = data["volumes"]["images"]
|
|
self.assertEqual(vol["name"], "app_images")
|
|
self.assertEqual(vol["driver"], "local")
|
|
self.assertEqual(vol["driver_opts"]["type"], "none")
|
|
self.assertEqual(vol["driver_opts"]["o"], "bind")
|
|
self.assertEqual(vol["driver_opts"]["device"], f"{_DIR_VAR_LIB}/app_images")
|
|
self.assertNotIn("nfs", vol)
|
|
|
|
def test_swarm_nfs_false_opt_out_stays_plain_local_volume(self):
|
|
apps = self._base_apps()
|
|
rendered = _call(
|
|
apps,
|
|
"app",
|
|
extra_volumes={
|
|
"repositories": {"name": "app_repositories", "nfs": False},
|
|
"shared": {"name": "app_shared"},
|
|
},
|
|
deployment_mode="swarm",
|
|
storage={
|
|
"backend": "nfs",
|
|
"nfs": {"server": "10.0.0.20", "export_base": "/srv/nfs"},
|
|
},
|
|
)
|
|
data = self._parse_yaml(rendered)
|
|
|
|
repositories = data["volumes"]["repositories"]
|
|
self.assertEqual(repositories, {"name": "app_repositories"})
|
|
shared = data["volumes"]["shared"]
|
|
self.assertEqual(shared["driver"], "local")
|
|
self.assertEqual(shared["driver_opts"]["device"], f"{_DIR_VAR_LIB}/app_shared")
|
|
|
|
def test_swarm_nfs_false_opt_out_from_meta_volumes(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["volumes"] = {
|
|
"repositories": {
|
|
"type": "volume",
|
|
"name": "app_repositories",
|
|
"nfs": False,
|
|
"mounts": [{"service": "gitaly", "target": "/home/git/repositories"}],
|
|
},
|
|
"shared": {
|
|
"type": "volume",
|
|
"name": "app_shared",
|
|
"mounts": [{"service": "web", "target": "/srv/app/shared"}],
|
|
},
|
|
}
|
|
rendered = _call(
|
|
apps,
|
|
"app",
|
|
deployment_mode="swarm",
|
|
storage={
|
|
"backend": "nfs",
|
|
"nfs": {"server": "10.0.0.20", "export_base": "/srv/nfs"},
|
|
},
|
|
)
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertEqual(data["volumes"]["repositories"], {"name": "app_repositories"})
|
|
self.assertIn("driver_opts", data["volumes"]["shared"])
|
|
|
|
def test_swarm_meta_nfs_false_survives_extra_volume_with_same_key(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["volumes"] = {
|
|
"scratch": {
|
|
"type": "volume",
|
|
"name": "app_scratch",
|
|
"nfs": False,
|
|
"mounts": [{"service": "recorder", "target": "/tmp"}],
|
|
},
|
|
"shared_tmp": {
|
|
"type": "volume",
|
|
"name": "app_shared_tmp",
|
|
"mounts": [{"service": "board", "target": "/tmp"}],
|
|
},
|
|
}
|
|
swarm_nfs = {
|
|
"deployment_mode": "swarm",
|
|
"storage": {
|
|
"backend": "nfs",
|
|
"nfs": {"server": "10.0.0.20", "export_base": "/srv/nfs"},
|
|
},
|
|
}
|
|
stack = self._parse_yaml(
|
|
_call(
|
|
apps,
|
|
"app",
|
|
extra_volumes={
|
|
"scratch": {"name": "app_scratch"},
|
|
"shared_tmp": {"name": "app_shared_tmp"},
|
|
},
|
|
**swarm_nfs,
|
|
)
|
|
)
|
|
prep = self._parse_yaml(_call(apps, "app", **swarm_nfs))
|
|
|
|
self.assertEqual(stack["volumes"]["scratch"], {"name": "app_scratch"})
|
|
self.assertEqual(stack["volumes"]["scratch"], prep["volumes"]["scratch"])
|
|
self.assertEqual(
|
|
stack["volumes"]["shared_tmp"]["driver_opts"]["device"],
|
|
f"{_DIR_VAR_LIB}/app_shared_tmp",
|
|
)
|
|
self.assertEqual(stack["volumes"]["shared_tmp"], prep["volumes"]["shared_tmp"])
|
|
|
|
def test_swarm_nfs_dict_value_keeps_rewrite(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["volumes"] = {
|
|
"data": {
|
|
"type": "volume",
|
|
"name": "app_data",
|
|
"nfs": {"uid": 1000, "gid": 1000, "mode": "0750"},
|
|
"mounts": [{"service": "web", "target": "/data"}],
|
|
},
|
|
}
|
|
rendered = _call(
|
|
apps,
|
|
"app",
|
|
deployment_mode="swarm",
|
|
storage={
|
|
"backend": "nfs",
|
|
"nfs": {"server": "10.0.0.20", "export_base": "/srv/nfs"},
|
|
},
|
|
)
|
|
data = self._parse_yaml(rendered)
|
|
|
|
vol = data["volumes"]["data"]
|
|
self.assertEqual(vol["driver_opts"]["device"], f"{_DIR_VAR_LIB}/app_data")
|
|
self.assertNotIn("nfs", vol)
|
|
self.assertEqual(
|
|
vol["x-infinito-nfs"], {"uid": 1000, "gid": 1000, "mode": "0750"}
|
|
)
|
|
|
|
def test_compose_mode_nfs_false_leaves_no_nfs_key(self):
|
|
apps = self._base_apps()
|
|
rendered = _call(
|
|
apps,
|
|
"app",
|
|
extra_volumes={"repositories": {"name": "app_repositories", "nfs": False}},
|
|
deployment_mode="compose",
|
|
storage={"backend": "local"},
|
|
)
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertEqual(data["volumes"]["repositories"], {"name": "app_repositories"})
|
|
|
|
def test_swarm_pinned_role_stays_node_local(self):
|
|
apps = self._base_apps()
|
|
with mock.patch(
|
|
"utils.roles.meta_lookup.get_role_placement",
|
|
return_value="manager",
|
|
):
|
|
rendered = _call(
|
|
apps,
|
|
"app",
|
|
extra_volumes={
|
|
"images": {"name": "app_images"},
|
|
"data": {"name": "app_data"},
|
|
},
|
|
deployment_mode="swarm",
|
|
storage={
|
|
"backend": "nfs",
|
|
"nfs": {"server": "10.0.0.20", "export_base": "/srv/nfs"},
|
|
},
|
|
)
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertNotIn("driver_opts", data["volumes"]["images"])
|
|
self.assertNotIn("driver_opts", data["volumes"]["data"])
|
|
|
|
def test_compose_mode_ignores_nfs_flag(self):
|
|
apps = self._base_apps()
|
|
rendered = _call(
|
|
apps,
|
|
"app",
|
|
extra_volumes={"images": {"name": "app_images", "nfs": True}},
|
|
deployment_mode="compose",
|
|
storage={
|
|
"backend": "nfs",
|
|
"nfs": {"server": "10.0.0.20", "export_base": "/srv/nfs"},
|
|
},
|
|
)
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertNotIn("driver_opts", data["volumes"]["images"])
|
|
self.assertNotIn("nfs", data["volumes"]["images"])
|
|
|
|
def test_swarm_local_backend_ignores_nfs_flag(self):
|
|
apps = self._base_apps()
|
|
rendered = _call(
|
|
apps,
|
|
"app",
|
|
extra_volumes={"images": {"name": "app_images", "nfs": True}},
|
|
deployment_mode="swarm",
|
|
storage={"backend": "local"},
|
|
)
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertNotIn("driver_opts", data["volumes"]["images"])
|
|
self.assertNotIn("nfs", data["volumes"]["images"])
|
|
|
|
def test_config_with_all_mounts_gated_off_is_not_declared(self):
|
|
apps = self._base_apps()
|
|
apps["app"]["volumes"] = {
|
|
"auth_ldap": {
|
|
"type": "config",
|
|
"source": "/opt/compose/app/volumes/auth_ldap.xml",
|
|
"mounts": [
|
|
{
|
|
"service": "backend",
|
|
"target": "/etc/auth_ldap.xml",
|
|
"when": False,
|
|
}
|
|
],
|
|
},
|
|
"proxy_conf": {
|
|
"type": "config",
|
|
"source": "/opt/compose/app/volumes/default.conf",
|
|
"mounts": [
|
|
{"service": "proxy", "target": "/etc/nginx/conf.d/default.conf"}
|
|
],
|
|
},
|
|
}
|
|
rendered = _call(apps, "app")
|
|
data = self._parse_yaml(rendered)
|
|
|
|
self.assertIn("configs", data)
|
|
self.assertNotIn("auth_ldap", data["configs"])
|
|
self.assertIn("proxy_conf", data["configs"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|