feat(filters): add active_docker_container_count filter and use it for fair resource splits

Compute per-container CPU/RAM shares based on active services (web-/svc-*, enabled=true or undefined). Cast host facts to numbers, add safe min=1, and output compose-ready values. Include robust unit test.

Also: include resource.yml.j2 in base template and minor formatting tidy-up.

https://chatgpt.com/share/68d2d96c-9bf4-800f-bbec-d4f2c0051c06
This commit is contained in:
2025-09-23 21:35:12 +02:00
parent c523d8d8d4
commit ff7b7aeb2d
6 changed files with 284 additions and 2 deletions

View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
"""
Ansible filter to count active docker services for current host.
Active means:
- application key is in group_names
- application key matches prefix regex (default: ^(web-|svc-).* )
- under applications[app]['docker']['services'] each service is counted if:
- 'enabled' is True, OR
- 'enabled' is missing/undefined (treated as active)
Returns an integer. If ensure_min_one=True, returns at least 1.
"""
import re
from typing import Any, Dict, Mapping, Iterable
def _is_mapping(x: Any) -> bool:
# be liberal: Mapping covers dict-like; fallback to dict check
try:
return isinstance(x, Mapping)
except Exception:
return isinstance(x, dict)
def active_docker_container_count(applications: Mapping[str, Any],
group_names: Iterable[str],
prefix_regex: str = r'^(web-|svc-).*',
ensure_min_one: bool = False) -> int:
if not _is_mapping(applications):
return 1 if ensure_min_one else 0
group_set = set(group_names or [])
try:
pattern = re.compile(prefix_regex)
except re.error:
pattern = re.compile(r'^(web-|svc-).*') # fallback
count = 0
for app_key, app_val in applications.items():
# host selection + name prefix
if app_key not in group_set:
continue
if not pattern.match(str(app_key)):
continue
docker = app_val.get('docker') if _is_mapping(app_val) else None
services = docker.get('services') if _is_mapping(docker) else None
if not _is_mapping(services):
# sometimes roles define a single service name string; ignore
continue
for _svc_name, svc_cfg in services.items():
if not _is_mapping(svc_cfg):
# allow shorthand like: service: {} or image string -> counts as enabled
count += 1
continue
enabled = svc_cfg.get('enabled', True)
if isinstance(enabled, bool):
if enabled:
count += 1
else:
# non-bool enabled -> treat "truthy" as enabled
if bool(enabled):
count += 1
if ensure_min_one and count < 1:
return 1
return count
class FilterModule(object):
def filters(self):
return {
# usage: {{ applications | active_docker_container_count(group_names) }}
'active_docker_container_count': active_docker_container_count,
}