diff --git a/roles/sys-ctl-hlth-webserver/filter_plugins/web_health_expectations.py b/roles/sys-ctl-hlth-webserver/filter_plugins/web_health_expectations.py index cc36b4e6..f217db55 100644 --- a/roles/sys-ctl-hlth-webserver/filter_plugins/web_health_expectations.py +++ b/roles/sys-ctl-hlth-webserver/filter_plugins/web_health_expectations.py @@ -1,4 +1,3 @@ -# roles/sys-ctl-hlth-webserver/filter_plugins/web_health_expectations.py import os import sys from collections.abc import Mapping @@ -94,6 +93,26 @@ def _normalize_selection(group_names): raise ValueError("web_health_expectations: 'group_names' must be provided and non-empty") return sel +def _normalize_codes(x): + """ + Accepts: + - single code (int or str) + - list/tuple/set of codes + Returns a de-duplicated list of valid ints (100..599) in original order. + """ + if x is None: + return [] + if isinstance(x, (list, tuple, set)): + out = [] + seen = set() + for v in x: + c = _valid_http_code(v) + if c is not None and c not in seen: + seen.add(c) + out.append(c) + return out + c = _valid_http_code(x) + return [c] if c is not None else [] def web_health_expectations(applications, www_enabled: bool = False, group_names=None, redirect_maps=None): """Produce a **flat mapping**: domain -> [expected_status_codes]. @@ -138,17 +157,15 @@ def web_health_expectations(applications, www_enabled: bool = False, group_names sc_map = {} if isinstance(sc_raw, Mapping): for k, v in sc_raw.items(): - code = _valid_http_code(v) - if code is not None: - sc_map[str(k)] = code + codes = _normalize_codes(v) + if codes: + sc_map[str(k)] = codes if isinstance(canonical_raw, Mapping) and canonical_raw: for key, domains in canonical_raw.items(): domains_list = _to_list(domains, allow_mapping=False) - code = _valid_http_code(sc_map.get(key)) - if code is None: - code = _valid_http_code(sc_map.get("default")) - expected = [code] if code is not None else list(DEFAULT_OK) + codes = sc_map.get(key) or sc_map.get("default") + expected = list(codes) if codes else list(DEFAULT_OK) for d in domains_list: if d: expectations[d] = expected @@ -156,8 +173,8 @@ def web_health_expectations(applications, www_enabled: bool = False, group_names for d in _to_list(canonical_raw, allow_mapping=True): if not d: continue - code = _valid_http_code(sc_map.get("default")) - expectations[d] = [code] if code is not None else list(DEFAULT_OK) + codes = sc_map.get("default") + expectations[d] = list(codes) if codes else list(DEFAULT_OK) for d in aliases: if d: diff --git a/roles/web-app-flowise/config/main.yml b/roles/web-app-flowise/config/main.yml index 9d5eef25..aa0b6855 100644 --- a/roles/web-app-flowise/config/main.yml +++ b/roles/web-app-flowise/config/main.yml @@ -12,38 +12,51 @@ server: - "flow.ai.{{ PRIMARY_DOMAIN }}" aliases: [] csp: - flags: {} - #script-src-elem: - # unsafe-inline: true - #script-src: - # unsafe-inline: true - # unsafe-eval: true - #style-src: - # unsafe-inline: true + flags: + script-src-elem: + unsafe-inline: true whitelist: font-src: + - https://fonts.gstatic.com + style-src-elem: - https://fonts.googleapis.com - connect-src: [] + script-src-elem: + - https://fonts.googleapis.com + - https://fonts.gstatic.com + - https://r.wdfl.co + connect-src: [] docker: services: litellm: backup: no_stop_required: true - image: ghcr.io/berriai/litellm - version: main-v1.77.3.dynamic_rates - name: litellm + image: ghcr.io/berriai/litellm + version: main-v1.77.3.dynamic_rates + name: litellm + cpus: "1.0" + mem_reservation: "0.5g" + mem_limit: "1g" + pids_limit: 1024 qdrant: backup: no_stop_required: true - image: qdrant/qdrant - version: latest - name: qdrant + image: qdrant/qdrant + version: latest + name: qdrant + cpus: "2.0" + mem_reservation: "2g" + mem_limit: "4g" + pids_limit: 2048 flowise: backup: - no_stop_required: true - image: flowiseai/flowise - version: latest - name: flowise + no_stop_required: false # As long as SQLite is used + image: flowiseai/flowise + version: latest + name: flowise + cpus: "1.0" + mem_reservation: "1g" + mem_limit: "2g" + pids_limit: 1024 redis: enabled: false database: diff --git a/roles/web-app-flowise/templates/docker-compose.yml.j2 b/roles/web-app-flowise/templates/docker-compose.yml.j2 index 07d7e83a..40361c92 100644 --- a/roles/web-app-flowise/templates/docker-compose.yml.j2 +++ b/roles/web-app-flowise/templates/docker-compose.yml.j2 @@ -1,5 +1,6 @@ {% include 'roles/docker-compose/templates/base.yml.j2' %} litellm: +{% set service_name = 'litellm' %} {% include 'roles/docker-container/templates/base.yml.j2' %} image: {{ FLOWISE_LITELLM_IMAGE }}:{{ FLOWISE_LITELLM_VERSION }} container_name: {{ FLOWISE_LITELLM_CONTAINER }} @@ -14,6 +15,7 @@ {% include 'roles/docker-container/templates/networks.yml.j2' %} qdrant: +{% set service_name = 'qdrant' %} {% include 'roles/docker-container/templates/base.yml.j2' %} image: {{ FLOWISE_QDRANT_IMAGE }}:{{ FLOWISE_QDRANT_VERSION }} container_name: {{ FLOWISE_QDRANT_CONTAINER }} @@ -25,6 +27,7 @@ {% include 'roles/docker-container/templates/networks.yml.j2' %} flowise: +{% set service_name = 'flowise' %} {% include 'roles/docker-container/templates/base.yml.j2' %} image: {{ FLOWISE_IMAGE }}:{{ FLOWISE_VERSION }} container_name: {{ FLOWISE_CONTAINER }} diff --git a/roles/web-app-minio/config/main.yml b/roles/web-app-minio/config/main.yml index c370c19b..d32e7a1a 100644 --- a/roles/web-app-minio/config/main.yml +++ b/roles/web-app-minio/config/main.yml @@ -10,7 +10,8 @@ features: ldap: false # OIDC is already activated so LDAP isn't necessary server: status_codes: - api: 400 + api: 301 + console: 301 domains: canonical: console: "console.s3.{{ PRIMARY_DOMAIN }}" @@ -25,10 +26,14 @@ docker: services: minio: backup: - no_stop_required: true - image: quay.io/minio/minio - version: latest - name: minio + no_stop_required: false + image: quay.io/minio/minio + version: latest + name: minio + cpus: "2.0" + mem_reservation: "2g" + mem_limit: "4g" + pids_limit: 2048 redis: enabled: false database: diff --git a/roles/web-app-pretix/config/main.yml b/roles/web-app-pretix/config/main.yml index 565fca0d..da084cc5 100644 --- a/roles/web-app-pretix/config/main.yml +++ b/roles/web-app-pretix/config/main.yml @@ -11,6 +11,10 @@ docker: name: pretix backup: no_stop_required: true + cpus: "2.0" + mem_reservation: "1.5g" + mem_limit: "2g" + pids_limit: 1024 volumes: data: "pretix_data" config: "pretix_config" diff --git a/tests/unit/roles/sys-ctl-hlth-webserver/filter_plugins/test_web_health_expectations.py b/tests/unit/roles/sys-ctl-hlth-webserver/filter_plugins/test_web_health_expectations.py index 1eb828b6..8c2b22e3 100644 --- a/tests/unit/roles/sys-ctl-hlth-webserver/filter_plugins/test_web_health_expectations.py +++ b/tests/unit/roles/sys-ctl-hlth-webserver/filter_plugins/test_web_health_expectations.py @@ -1,4 +1,3 @@ -# tests/unit/roles/sys-ctl-hlth-webserver/filter_plugins/test_web_health_expectations.py import os import unittest import importlib.util @@ -272,7 +271,131 @@ class TestWebHealthExpectationsFilter(unittest.TestCase): ) self.assertNotIn("ignored.example.org", out) self.assertEqual(out["manual.example.org"], [301]) + + # --------- NEW: status_codes list support --------- + def test_flat_canonical_with_default_list(self): + apps = {"app-l1": {}} + self._configure_returns({ + ("app-l1", "server.domains.canonical"): ["l1.example.org"], + ("app-l1", "server.domains.aliases"): [], + ("app-l1", "server.status_codes"): {"default": [204, "302", 301]}, + }) + out = self.mod.web_health_expectations(apps, group_names=["app-l1"]) + self.assertEqual(out["l1.example.org"], [204, 302, 301]) + + def test_keyed_canonical_with_list_and_default_list(self): + apps = {"app-l2": {}} + self._configure_returns({ + ("app-l2", "server.domains.canonical"): { + "api": ["api1.l2.example.org", "api2.l2.example.org"], + "web": "web.l2.example.org", + }, + ("app-l2", "server.domains.aliases"): [], + ("app-l2", "server.status_codes"): {"api": [301, 403], "default": [200, 204]}, + }) + out = self.mod.web_health_expectations(apps, group_names=["app-l2"]) + self.assertEqual(out["api1.l2.example.org"], [301, 403]) # per-key list wins + self.assertEqual(out["api2.l2.example.org"], [301, 403]) + self.assertEqual(out["web.l2.example.org"], [200, 204]) # default list + + def test_status_codes_strings_and_ints_and_out_of_range_ignored(self): + apps = {"app-l3": {}} + # 99 (<100) and 700 (>599) are ignored, "301" string is converted + self._configure_returns({ + ("app-l3", "server.domains.canonical"): ["l3.example.org"], + ("app-l3", "server.domains.aliases"): [], + ("app-l3", "server.status_codes"): {"default": ["301", 200, 99, 700]}, + }) + out = self.mod.web_health_expectations(apps, group_names=["app-l3"]) + self.assertEqual(out["l3.example.org"], [301, 200]) + + def test_status_codes_deduplicate_preserve_order(self): + apps = {"app-l4": {}} + self._configure_returns({ + ("app-l4", "server.domains.canonical"): ["l4.example.org"], + ("app-l4", "server.domains.aliases"): [], + ("app-l4", "server.status_codes"): {"default": [301, 302, 301, 302, 200]}, + }) + out = self.mod.web_health_expectations(apps, group_names=["app-l4"]) + self.assertEqual(out["l4.example.org"], [301, 302, 200]) # dedup but keep order + + def test_key_specific_int_overrides_default_list(self): + apps = {"app-l5": {}} + self._configure_returns({ + ("app-l5", "server.domains.canonical"): {"console": ["c1.l5.example.org"]}, + ("app-l5", "server.domains.aliases"): [], + ("app-l5", "server.status_codes"): {"console": 301, "default": [200, 204]}, + }) + out = self.mod.web_health_expectations(apps, group_names=["app-l5"]) + self.assertEqual(out["c1.l5.example.org"], [301]) # per-key int beats default list + + def test_key_specific_list_overrides_default_int(self): + apps = {"app-l6": {}} + self._configure_returns({ + ("app-l6", "server.domains.canonical"): {"api": "api.l6.example.org"}, + ("app-l6", "server.domains.aliases"): [], + ("app-l6", "server.status_codes"): {"api": [301, 403], "default": 200}, + }) + out = self.mod.web_health_expectations(apps, group_names=["app-l6"]) + self.assertEqual(out["api.l6.example.org"], [301, 403]) + + def test_invalid_default_list_falls_back_to_DEFAULT_OK(self): + apps = {"app-l7": {}} + # everything invalid → fall back to DEFAULT_OK + self._configure_returns({ + ("app-l7", "server.domains.canonical"): ["l7.example.org"], + ("app-l7", "server.domains.aliases"): [], + ("app-l7", "server.status_codes"): {"default": ["x", 42.42, {}, 700, 99]}, + }) + out = self.mod.web_health_expectations(apps, group_names=["app-l7"]) + self.assertEqual(out["l7.example.org"], [200, 302, 301]) + + def test_key_with_invalid_list_uses_default_list(self): + apps = {"app-l8": {}} + self._configure_returns({ + ("app-l8", "server.domains.canonical"): {"web": "web.l8.example.org"}, + ("app-l8", "server.domains.aliases"): [], + ("app-l8", "server.status_codes"): {"web": ["foo", None], "default": [204, 206]}, + }) + out = self.mod.web_health_expectations(apps, group_names=["app-l8"]) + self.assertEqual(out["web.l8.example.org"], [204, 206]) + + def test_key_and_default_both_invalid_falls_back_to_DEFAULT_OK(self): + apps = {"app-l9": {}} + self._configure_returns({ + ("app-l9", "server.domains.canonical"): {"api": "api.l9.example.org"}, + ("app-l9", "server.domains.aliases"): [], + ("app-l9", "server.status_codes"): {"api": ["bad"], "default": ["also", "bad"]}, + }) + out = self.mod.web_health_expectations(apps, group_names=["app-l9"]) + self.assertEqual(out["api.l9.example.org"], [200, 302, 301]) + + def test_aliases_still_forced_to_301_even_with_default_list(self): + apps = {"app-l10": {}} + self._configure_returns({ + ("app-l10", "server.domains.canonical"): ["l10.example.org"], + ("app-l10", "server.domains.aliases"): ["alias.l10.example.org"], + ("app-l10", "server.status_codes"): {"default": [204, 206]}, + }) + out = self.mod.web_health_expectations(apps, group_names=["app-l10"]) + self.assertEqual(out["l10.example.org"], [204, 206]) + self.assertEqual(out["alias.l10.example.org"], [301]) + + def test_keyed_canonical_with_mixed_scalar_and_list_domains(self): + apps = {"app-l11": {}} + self._configure_returns({ + ("app-l11", "server.domains.canonical"): { + "api": "api.l11.example.org", + "view": ["v1.l11.example.org", "v2.l11.example.org"], + }, + ("app-l11", "server.domains.aliases"): [], + ("app-l11", "server.status_codes"): {"view": [301, 307], "default": [200, 204]}, + }) + out = self.mod.web_health_expectations(apps, group_names=["app-l11"]) + self.assertEqual(out["api.l11.example.org"], [200, 204]) # default + self.assertEqual(out["v1.l11.example.org"], [301, 307]) # per-key list + self.assertEqual(out["v2.l11.example.org"], [301, 307]) if __name__ == "__main__": unittest.main()