mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-11-20 12:06:25 +00:00
Compare commits
3 Commits
9bf77e1e35
...
c181c7f6cd
| Author | SHA1 | Date | |
|---|---|---|---|
| c181c7f6cd | |||
| 929cddec0e | |||
| 9ba0efc1a1 |
40
filter_plugins/resource_filter.py
Normal file
40
filter_plugins/resource_filter.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# filter_plugins/resource_filter.py
|
||||
from __future__ import annotations
|
||||
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
from module_utils.config_utils import get_app_conf, AppConfigKeyError, ConfigEntryNotSetError # noqa: F401
|
||||
from module_utils.entity_name_utils import get_entity_name
|
||||
|
||||
from ansible.errors import AnsibleFilterError
|
||||
|
||||
|
||||
def resource_filter(
|
||||
applications: dict,
|
||||
application_id: str,
|
||||
key: str,
|
||||
service_name: str,
|
||||
hard_default,
|
||||
):
|
||||
"""
|
||||
Lookup order:
|
||||
1) docker.services.<service_name or get_entity_name(application_id)>.<key>
|
||||
2) hard_default (mandatory)
|
||||
|
||||
- service_name may be "" → will resolve to get_entity_name(application_id).
|
||||
- hard_default is mandatory (no implicit None).
|
||||
- required=False always.
|
||||
"""
|
||||
try:
|
||||
primary_service = service_name if service_name != "" else get_entity_name(application_id)
|
||||
return get_app_conf(applications, application_id, f"docker.services.{primary_service}.{key}", False, hard_default)
|
||||
except (AppConfigKeyError, ConfigEntryNotSetError) as e:
|
||||
raise AnsibleFilterError(str(e))
|
||||
|
||||
|
||||
class FilterModule(object):
|
||||
def filters(self):
|
||||
return {
|
||||
"resource_filter": resource_filter,
|
||||
}
|
||||
@@ -29,4 +29,31 @@ NGINX:
|
||||
IMAGE: "/tmp/cache_nginx_image/" # Directory which nginx uses to cache images
|
||||
USER: "http" # Default nginx user in ArchLinux
|
||||
|
||||
# Effective CPUs (float) across proxy and the current app
|
||||
WEBSERVER_CPUS_EFFECTIVE: >-
|
||||
{{
|
||||
[
|
||||
(applications | resource_filter('svc-prx-openresty', 'cpus', service_name | default(''), RESOURCE_CPUS)) | float,
|
||||
(applications | resource_filter(application_id, 'cpus', service_name | default(''), RESOURCE_CPUS)) | float
|
||||
] | min
|
||||
}}
|
||||
|
||||
# Nginx requires an integer for worker_processes:
|
||||
# - if cpus < 1 → 1
|
||||
# - else → floor to int
|
||||
WEBSERVER_WORKER_PROCESSES: >-
|
||||
{{
|
||||
1 if (WEBSERVER_CPUS_EFFECTIVE | float) < 1
|
||||
else (WEBSERVER_CPUS_EFFECTIVE | float | int)
|
||||
}}
|
||||
|
||||
# worker_connections from pids_limit (use the smaller one), with correct key/defaults
|
||||
WEBSERVER_WORKER_CONNECTIONS: >-
|
||||
{{
|
||||
[
|
||||
(applications | resource_filter('svc-prx-openresty', 'pids_limit', service_name | default(''), RESOURCE_PIDS_LIMIT)) | int,
|
||||
(applications | resource_filter(application_id, 'pids_limit', service_name | default(''), RESOURCE_PIDS_LIMIT)) | int
|
||||
] | min
|
||||
}}
|
||||
|
||||
# @todo It propably makes sense to distinguish between target and source mount path, so that the config files can be stored in the openresty volumes folder
|
||||
@@ -1,4 +1,4 @@
|
||||
cpus: {{ applications | get_app_conf( application_id, [ 'docker', 'services', service_name | default(application_id | get_entity_name ), 'cpus' ] |join('.'), False, RESOURCE_CPUS) }}
|
||||
mem_reservation: {{ applications | get_app_conf( application_id, [ 'docker', 'services', service_name | default(application_id | get_entity_name ), 'mem_reservation' ] |join('.'), False, RESOURCE_MEM_RESERVATION) }}
|
||||
mem_limit: {{ applications | get_app_conf( application_id, [ 'docker', 'services', service_name | default(application_id | get_entity_name ), 'mem_limit' ] |join('.'), False, RESOURCE_MEM_LIMIT) }}
|
||||
pids_limit: {{ applications | get_app_conf( application_id, [ 'docker', 'services', service_name | default(application_id | get_entity_name ), 'pids_limit' ] |join('.'), False, RESOURCE_PIDS_LIMIT) }}
|
||||
cpus: {{ applications | resource_filter(application_id, 'cpus', service_name | default(''), RESOURCE_CPUS) }}
|
||||
mem_reservation: {{ applications | resource_filter(application_id, 'mem_reservation', service_name | default(''), RESOURCE_MEM_RESERVATION) }}
|
||||
mem_limit: {{ applications | resource_filter(application_id, 'mem_limit', service_name | default(''), RESOURCE_MEM_LIMIT) }}
|
||||
pids_limit: {{ applications | resource_filter(application_id, 'pids_limit', service_name | default(''), RESOURCE_PIDS_LIMIT) }}
|
||||
@@ -1,7 +1,10 @@
|
||||
docker:
|
||||
services:
|
||||
openresty:
|
||||
name: "openresty"
|
||||
name: "openresty"
|
||||
cpus: 0.5
|
||||
mem_reservation: 1g
|
||||
mem_limit: 2g
|
||||
volumes:
|
||||
www: "/var/www/"
|
||||
nginx: "/etc/nginx/"
|
||||
@@ -1,8 +1,8 @@
|
||||
worker_processes auto;
|
||||
worker_processes {{ WEBSERVER_WORKER_PROCESSES }};
|
||||
|
||||
events
|
||||
{
|
||||
worker_connections 1024;
|
||||
worker_connections {{ WEBSERVER_WORKER_CONNECTIONS }};
|
||||
}
|
||||
|
||||
http
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
worker_processes auto;
|
||||
worker_processes {{ WEBSERVER_WORKER_PROCESSES }};
|
||||
|
||||
events { worker_connections 1024; }
|
||||
events { worker_connections {{ WEBSERVER_WORKER_CONNECTIONS }}; }
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Verify time by time, that this rules are valid:
|
||||
# https://docs.nextcloud.com/server/latest/admin_manual/installation/nginx.html
|
||||
|
||||
worker_processes auto;
|
||||
worker_processes {{ WEBSERVER_WORKER_PROCESSES }};
|
||||
|
||||
# @see https://chatgpt.com/share/67aa3ce9-eea0-800f-85e8-ac54a3810b13
|
||||
error_log /proc/self/fd/2 {% if MODE_DEBUG | bool %}debug{% else %}warn{% endif %};
|
||||
@@ -10,7 +10,7 @@ pid /var/run/nginx.pid;
|
||||
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
worker_connections {{ WEBSERVER_WORKER_CONNECTIONS }};
|
||||
}
|
||||
|
||||
|
||||
|
||||
133
tests/unit/filter_plugins/test_resource_filter.py
Normal file
133
tests/unit/filter_plugins/test_resource_filter.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# tests/unit/filter_plugins/test_resource_filter.py
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
import importlib
|
||||
|
||||
# Import the plugin module under test
|
||||
plugin_module = importlib.import_module("filter_plugins.resource_filter")
|
||||
|
||||
|
||||
class TestResourceFilter(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Reload to ensure a clean module state for each test
|
||||
importlib.reload(plugin_module)
|
||||
|
||||
self.applications = {"some": "dict"}
|
||||
self.application_id = "web-app-foo"
|
||||
self.key = "cpus"
|
||||
|
||||
# Patch get_app_conf and get_entity_name inside the plugin module
|
||||
self.patcher_conf = patch.object(plugin_module, "get_app_conf")
|
||||
self.patcher_entity = patch.object(plugin_module, "get_entity_name")
|
||||
self.mock_get_app_conf = self.patcher_conf.start()
|
||||
self.mock_get_entity_name = self.patcher_entity.start()
|
||||
self.mock_get_entity_name.return_value = "foo" # derived service name
|
||||
|
||||
def tearDown(self):
|
||||
self.patcher_conf.stop()
|
||||
self.patcher_entity.stop()
|
||||
|
||||
def test_primary_service_value_found(self):
|
||||
"""Returns the value when get_app_conf finds it for an explicit service."""
|
||||
self.mock_get_app_conf.return_value = "0.75"
|
||||
|
||||
result = plugin_module.resource_filter(
|
||||
self.applications,
|
||||
self.application_id,
|
||||
self.key,
|
||||
service_name="openresty",
|
||||
hard_default="0.5",
|
||||
)
|
||||
|
||||
self.assertEqual(result, "0.75")
|
||||
self.mock_get_app_conf.assert_called_once_with(
|
||||
self.applications,
|
||||
self.application_id,
|
||||
"docker.services.openresty.cpus",
|
||||
False,
|
||||
"0.5",
|
||||
)
|
||||
|
||||
def test_service_name_empty_uses_get_entity_name(self):
|
||||
"""When service_name is empty, it resolves via get_entity_name(application_id)."""
|
||||
self.mock_get_app_conf.return_value = "1.0"
|
||||
|
||||
result = plugin_module.resource_filter(
|
||||
self.applications,
|
||||
self.application_id,
|
||||
self.key,
|
||||
service_name="",
|
||||
hard_default="0.5",
|
||||
)
|
||||
|
||||
self.assertEqual(result, "1.0")
|
||||
self.mock_get_entity_name.assert_called_once_with(self.application_id)
|
||||
self.mock_get_app_conf.assert_called_once_with(
|
||||
self.applications,
|
||||
self.application_id,
|
||||
"docker.services.foo.cpus",
|
||||
False,
|
||||
"0.5",
|
||||
)
|
||||
|
||||
def test_returns_hard_default_when_missing(self):
|
||||
"""
|
||||
If the primary value is missing, get_app_conf (strict=False) should return the provided
|
||||
default. We simulate that by returning the default value directly from the mock.
|
||||
"""
|
||||
self.mock_get_app_conf.return_value = "2g"
|
||||
|
||||
result = plugin_module.resource_filter(
|
||||
self.applications,
|
||||
self.application_id,
|
||||
key="mem_limit",
|
||||
service_name="openresty",
|
||||
hard_default="2g",
|
||||
)
|
||||
|
||||
self.assertEqual(result, "2g")
|
||||
self.mock_get_app_conf.assert_called_once_with(
|
||||
self.applications,
|
||||
self.application_id,
|
||||
"docker.services.openresty.mem_limit",
|
||||
False,
|
||||
"2g",
|
||||
)
|
||||
|
||||
def test_hard_default_passthrough_type(self):
|
||||
"""Ensure the hard_default (including non-string types) is passed through correctly."""
|
||||
self.mock_get_app_conf.return_value = 2048 # simulate get_app_conf returning the default
|
||||
|
||||
result = plugin_module.resource_filter(
|
||||
self.applications,
|
||||
self.application_id,
|
||||
key="pids_limit",
|
||||
service_name="openresty",
|
||||
hard_default=2048,
|
||||
)
|
||||
|
||||
self.assertEqual(result, 2048)
|
||||
self.mock_get_app_conf.assert_called_once_with(
|
||||
self.applications,
|
||||
self.application_id,
|
||||
"docker.services.openresty.pids_limit",
|
||||
False,
|
||||
2048,
|
||||
)
|
||||
|
||||
def test_raises_ansible_filter_error_on_config_errors(self):
|
||||
"""Underlying config errors must be wrapped as AnsibleFilterError."""
|
||||
self.mock_get_app_conf.side_effect = plugin_module.AppConfigKeyError("bad path")
|
||||
|
||||
with self.assertRaises(plugin_module.AnsibleFilterError):
|
||||
plugin_module.resource_filter(
|
||||
self.applications,
|
||||
self.application_id,
|
||||
key="pids_limit",
|
||||
service_name="openresty",
|
||||
hard_default=2048,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user