feat(filters): enforce safe Node.js heap sizing via reusable filter

- Add node_autosize filter (node_max_old_space_size) using get_app_conf
- Raise error when mem_limit < min_mb to prevent OOM-kill misconfigurations
- Wire Whiteboard NODE_OPTIONS and increase mem_limit to 1g; set cpus=1
- Refactor PeerTube to use the same filter; simplify vars
- Add unit tests; keep integration filters usage green

Context: https://chatgpt.com/share/690e0499-6a94-800f-b8ed-2c5124690103
This commit is contained in:
2025-11-07 15:39:54 +01:00
parent 493d5bbbda
commit 36f9573fdf
6 changed files with 231 additions and 31 deletions

View File

@@ -0,0 +1,80 @@
# tests/unit/filter_plugins/test_node_autosize.py
import unittest
from unittest.mock import patch
# Module under test
import filter_plugins.node_autosize as na
try:
from ansible.errors import AnsibleFilterError # type: ignore
except Exception:
AnsibleFilterError = Exception
class TestNodeAutosizeFilter(unittest.TestCase):
"""Unit tests for the node_autosize filter plugin."""
def setUp(self):
# Default parameters used by all tests
self.applications = {"web-app-nextcloud": {"docker": {"services": {"whiteboard": {}}}}}
self.application_id = "web-app-nextcloud"
self.service_name = "whiteboard"
# Patch get_app_conf (imported from module_utils.config_utils) inside the filter plugin
self.patcher = patch("filter_plugins.node_autosize.get_app_conf")
self.mock_get_app_conf = self.patcher.start()
def tearDown(self):
self.patcher.stop()
def _set_mem_limit(self, value):
"""Helper: mock get_app_conf to return a specific mem_limit value."""
def _fake_get_app_conf(applications, application_id, config_path, strict=True, default=None, **_kwargs):
assert application_id == self.application_id
assert config_path == f"docker.services.{self.service_name}.mem_limit"
return value
self.mock_get_app_conf.side_effect = _fake_get_app_conf
# --- Tests for node_max_old_space_size (MB) ---
def test_512m_below_minimum_raises(self):
# mem_limit=512 MB < min_mb=768 -> must raise
self._set_mem_limit("512m")
with self.assertRaises(AnsibleFilterError):
na.node_max_old_space_size(self.applications, self.application_id, self.service_name)
def test_2g_caps_to_minimum_768(self):
self._set_mem_limit("2g")
mb = na.node_max_old_space_size(self.applications, self.application_id, self.service_name)
self.assertEqual(mb, 768) # 35% of 2g = 700 < 768 -> min wins
def test_8g_uses_35_percent_without_hitting_hardcap(self):
self._set_mem_limit("8g")
mb = na.node_max_old_space_size(self.applications, self.application_id, self.service_name)
self.assertEqual(mb, 2800) # 8g -> 8000 MB * 0.35 = 2800
def test_16g_hits_hardcap_3072(self):
self._set_mem_limit("16g")
mb = na.node_max_old_space_size(self.applications, self.application_id, self.service_name)
self.assertEqual(mb, 3072) # 35% of 16g = 5600, hardcap=3072
def test_numeric_bytes_input(self):
# 2 GiB in bytes (IEC): 2 * 1024 ** 3 = 2147483648
self._set_mem_limit(2147483648)
mb = na.node_max_old_space_size(self.applications, self.application_id, self.service_name)
# 2 GiB ≈ 2147 MB; 35% => ~751, min 768 => 768
self.assertEqual(mb, 768)
def test_invalid_unit_raises_error(self):
self._set_mem_limit("12x") # invalid unit
with self.assertRaises(AnsibleFilterError):
na.node_max_old_space_size(self.applications, self.application_id, self.service_name)
def test_missing_mem_limit_raises_error(self):
self._set_mem_limit(None)
with self.assertRaises(AnsibleFilterError):
na.node_max_old_space_size(self.applications, self.application_id, self.service_name)
if __name__ == "__main__":
unittest.main()