mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
Merge pull request #97 from kevinveenbirkenbach/feature/general-optimation
Feature/general optimation
This commit is contained in:
19 files changed
+410
-68
No files matched your search
@@ -0,0 +1,94 @@
|
||||
## Summary
|
||||
|
||||
Briefly describe the purpose of this pull request.
|
||||
|
||||
Example:
|
||||
|
||||
* Add new role for application X
|
||||
* Fix configuration issue in role Y
|
||||
* Improve container startup logic
|
||||
|
||||
---
|
||||
|
||||
## Change Type
|
||||
|
||||
Select the semantic version impact of this change:
|
||||
|
||||
* [ ] **Major** – Breaking change (incompatible configuration or behaviour)
|
||||
* [ ] **Minor** – New feature or new role (backwards compatible)
|
||||
* [ ] **Patch** – Bugfix or internal improvement
|
||||
|
||||
---
|
||||
|
||||
## Impacted Areas
|
||||
|
||||
Select affected components.
|
||||
|
||||
* [ ] Role implementation
|
||||
* [ ] Configuration variables
|
||||
* [ ] Documentation
|
||||
* [ ] CI/CD
|
||||
* [ ] Security
|
||||
|
||||
---
|
||||
|
||||
## Description of Changes
|
||||
|
||||
Explain what was changed and why.
|
||||
|
||||
Key points:
|
||||
|
||||
* What problem does this solve?
|
||||
* Why was this implementation chosen?
|
||||
* Are there alternative approaches?
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
Please ensure all items are completed before requesting review.
|
||||
|
||||
### Workflow
|
||||
|
||||
* [ ] Contributions follow the [collaboration workflow](https://hub.infinito.nexus/t/working-with-folks-in-infinito-nexus/436).
|
||||
|
||||
### Code Quality
|
||||
|
||||
* [ ] Code follows repository conventions
|
||||
* [ ] Code and comments are written in English
|
||||
|
||||
### Functionality
|
||||
|
||||
* [ ] Feature works as expected
|
||||
|
||||
### Testing
|
||||
|
||||
* [ ] Changes tested locally
|
||||
* [ ] CI pipeline passes
|
||||
* [ ] E2E test introduced for web-* roles
|
||||
* [ ] Unit test integrated for *.py files
|
||||
* [ ] Integration test integrated for *.py files
|
||||
|
||||
### Documentation
|
||||
|
||||
* [ ] README.md's and other *.md updated
|
||||
* [ ] Link from this request to the https://project.infinito.nexus/ workitem and backwards
|
||||
* [ ] Workitem in https://project.infinito.nexus/ updated
|
||||
* [ ] Attach a **screenshot of the running application** from the local deployment.([📕local environment setup guide](https://hub.infinito.nexus/t/local-test-environment-deploy/435))
|
||||
|
||||
---
|
||||
|
||||
## Checklist for Reviewer
|
||||
|
||||
Reviewer should verify:
|
||||
|
||||
* [ ] Code is understandable
|
||||
* [ ] Changes follow project guidelines
|
||||
* [ ] Documentation is adequate
|
||||
* [ ] No obvious security issues
|
||||
|
||||
---
|
||||
|
||||
## Additional Notes
|
||||
|
||||
Add anything reviewers should know.
|
||||
@@ -39,7 +39,6 @@ class Compose:
|
||||
check: bool = True,
|
||||
capture: bool = False,
|
||||
live: bool = False,
|
||||
keep_lines: int = 400,
|
||||
text: bool = True,
|
||||
extra_env: dict[str, str] | None = None,
|
||||
) -> subprocess.CompletedProcess:
|
||||
@@ -49,9 +48,7 @@ class Compose:
|
||||
env.update({k: str(v) for k, v in extra_env.items()})
|
||||
|
||||
if live:
|
||||
r = run_streaming(
|
||||
cmd, cwd=self.repo_root, env=env, keep_lines=keep_lines, text=text
|
||||
)
|
||||
r = run_streaming(cmd, cwd=self.repo_root, env=env, text=text)
|
||||
else:
|
||||
r = subprocess.run(
|
||||
cmd,
|
||||
@@ -158,8 +155,7 @@ class Compose:
|
||||
["sh", "-lc", "/opt/src/infinito/scripts/docker/entry.sh true"],
|
||||
workdir="/opt/src/infinito",
|
||||
check=False,
|
||||
live=True, # <--- live output
|
||||
keep_lines=400, # <--- tail for failure printing
|
||||
live=True,
|
||||
extra_env={
|
||||
"ANSIBLE_FORCE_COLOR": "1",
|
||||
"PY_COLORS": "1",
|
||||
@@ -168,10 +164,6 @@ class Compose:
|
||||
)
|
||||
|
||||
if r.returncode != 0:
|
||||
print("===== entry.sh stdout (tail) =====")
|
||||
print((r.stdout or "").rstrip() or "<empty>")
|
||||
print("===== entry.sh stderr (tail) =====")
|
||||
print((r.stderr or "").rstrip() or "<empty>")
|
||||
raise RuntimeError(f"entry.sh init failed (rc={r.returncode})")
|
||||
|
||||
def down(self) -> None:
|
||||
@@ -193,7 +185,6 @@ class Compose:
|
||||
workdir: str | None = None,
|
||||
capture: bool = False,
|
||||
live: bool = False,
|
||||
keep_lines: int = 400,
|
||||
tty: bool | None = None,
|
||||
extra_env: dict[str, str] | None = None,
|
||||
) -> subprocess.CompletedProcess:
|
||||
@@ -206,7 +197,7 @@ class Compose:
|
||||
- tty=False -> -T (CI safe)
|
||||
|
||||
Streaming:
|
||||
- live=True streams stdout/stderr to terminal while keeping a tail buffer.
|
||||
- live=True streams stdout/stderr to terminal.
|
||||
"""
|
||||
if tty is None:
|
||||
tty = not self._is_ci()
|
||||
@@ -229,7 +220,6 @@ class Compose:
|
||||
check=check,
|
||||
capture=capture,
|
||||
live=live,
|
||||
keep_lines=keep_lines,
|
||||
)
|
||||
|
||||
def _get_infinito_container_id(self) -> str:
|
||||
|
||||
@@ -38,13 +38,11 @@ def _run_deploy(
|
||||
if passthrough:
|
||||
cmd.extend(passthrough)
|
||||
|
||||
# Live stream output for immediate visibility,
|
||||
# while keeping a tail buffer for post-failure printing.
|
||||
# Live stream output for immediate visibility.
|
||||
r = compose.exec(
|
||||
cmd,
|
||||
check=False,
|
||||
live=True,
|
||||
keep_lines=400,
|
||||
extra_env={
|
||||
# Force ANSI colors even when no TTY is allocated (CI default).
|
||||
"ANSIBLE_FORCE_COLOR": "1",
|
||||
@@ -53,13 +51,6 @@ def _run_deploy(
|
||||
},
|
||||
)
|
||||
|
||||
if r.returncode != 0:
|
||||
print("===== deploy stdout (tail) =====")
|
||||
print((r.stdout or "").rstrip() or "<empty>")
|
||||
print("===== deploy stderr (tail) =====")
|
||||
print((r.stderr or "").rstrip() or "<empty>")
|
||||
print("===============================")
|
||||
|
||||
return int(r.returncode)
|
||||
|
||||
|
||||
|
||||
@@ -3,24 +3,21 @@ from __future__ import annotations
|
||||
import sys
|
||||
import subprocess
|
||||
import threading
|
||||
from collections import deque
|
||||
from typing import Deque, TextIO
|
||||
from typing import TextIO
|
||||
|
||||
|
||||
def _drain_stream(
|
||||
stream: TextIO,
|
||||
*,
|
||||
sink: TextIO,
|
||||
buf: Deque[str],
|
||||
) -> None:
|
||||
"""
|
||||
Read a text stream line-by-line, write to sink, and keep a tail buffer.
|
||||
Read a text stream line-by-line and write to sink.
|
||||
"""
|
||||
try:
|
||||
for line in iter(stream.readline, ""):
|
||||
sink.write(line)
|
||||
sink.flush()
|
||||
buf.append(line.rstrip("\n"))
|
||||
finally:
|
||||
try:
|
||||
stream.close()
|
||||
@@ -34,13 +31,10 @@ def run_streaming(
|
||||
*,
|
||||
cwd,
|
||||
env: dict[str, str],
|
||||
keep_lines: int = 400,
|
||||
text: bool = True,
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""
|
||||
Run a subprocess, stream stdout/stderr live to the terminal, and return
|
||||
a CompletedProcess whose stdout/stderr contain only the last `keep_lines`
|
||||
lines (tail buffers).
|
||||
Run a subprocess and stream stdout/stderr live to the terminal.
|
||||
"""
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
@@ -52,19 +46,16 @@ def run_streaming(
|
||||
bufsize=1, # line buffered (best effort)
|
||||
)
|
||||
|
||||
out_buf: Deque[str] = deque(maxlen=int(keep_lines))
|
||||
err_buf: Deque[str] = deque(maxlen=int(keep_lines))
|
||||
|
||||
assert p.stdout is not None
|
||||
assert p.stderr is not None
|
||||
|
||||
t_out = threading.Thread(
|
||||
target=_drain_stream,
|
||||
kwargs={"stream": p.stdout, "sink": sys.stdout, "buf": out_buf},
|
||||
kwargs={"stream": p.stdout, "sink": sys.stdout},
|
||||
)
|
||||
t_err = threading.Thread(
|
||||
target=_drain_stream,
|
||||
kwargs={"stream": p.stderr, "sink": sys.stderr, "buf": err_buf},
|
||||
kwargs={"stream": p.stderr, "sink": sys.stderr},
|
||||
)
|
||||
|
||||
t_out.daemon = True
|
||||
@@ -78,7 +69,4 @@ def run_streaming(
|
||||
t_out.join()
|
||||
t_err.join()
|
||||
|
||||
stdout_tail = ("\n".join(out_buf) + ("\n" if out_buf else "")).rstrip("\n")
|
||||
stderr_tail = ("\n".join(err_buf) + ("\n" if err_buf else "")).rstrip("\n")
|
||||
|
||||
return subprocess.CompletedProcess(cmd, rc, stdout=stdout_tail, stderr=stderr_tail)
|
||||
return subprocess.CompletedProcess(cmd, rc)
|
||||
@@ -265,8 +265,7 @@ class FilterModule(object):
|
||||
):
|
||||
# Allow being embedded by the dashboard app domain's site
|
||||
domain = domains.get("web-app-dashboard")[0]
|
||||
sld_tld = ".".join(domain.split(".")[-2:]) # e.g., example.com
|
||||
tokens.append(f"{sld_tld}")
|
||||
tokens.append(f"{domain}")
|
||||
if self.is_feature_enabled(applications, "logout", application_id):
|
||||
tokens.append(get_url(domains, "web-svc-logout", web_protocol))
|
||||
tokens.append(
|
||||
|
||||
@@ -24,7 +24,7 @@ class FilterModule:
|
||||
|
||||
Ruby rules handled:
|
||||
- " -> \"
|
||||
- \ -> \\
|
||||
- \\ -> \\\\
|
||||
- newlines -> \n
|
||||
- carriage return -> \r
|
||||
- tab -> \t
|
||||
|
||||
@@ -32,7 +32,7 @@ def _render_if_template(templar, value, var_name: str):
|
||||
return value
|
||||
|
||||
try:
|
||||
rendered = templar.template(value, disable_lookups=False)
|
||||
rendered = templar.template(value)
|
||||
except TypeError:
|
||||
# Unit tests can inject a minimal templar without Ansible's full signature.
|
||||
rendered = templar.template(value)
|
||||
@@ -51,7 +51,7 @@ def _render_if_template(templar, value, var_name: str):
|
||||
def _resolve_version(templar, variables):
|
||||
# Try the canonical lookup call first.
|
||||
try:
|
||||
version = templar.template("{{ lookup('version') }}", disable_lookups=False)
|
||||
version = templar.template("{{ lookup('version') }}")
|
||||
except TypeError:
|
||||
# Unit tests can inject a minimal templar without Ansible's full signature.
|
||||
version = templar.template("{{ lookup('version') }}")
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
from module_utils.config_utils import get_app_conf
|
||||
|
||||
|
||||
def _require_non_empty(value: Any, name: str) -> str:
|
||||
s = str(value).strip()
|
||||
if not s:
|
||||
raise AnsibleError(f"service_should_load: '{name}' must not be empty")
|
||||
return s
|
||||
|
||||
|
||||
def _service_enabled_and_shared(
|
||||
applications: Dict[str, Any],
|
||||
application_id: str,
|
||||
service_name: str,
|
||||
default: bool,
|
||||
) -> bool:
|
||||
enabled = get_app_conf(
|
||||
applications=applications,
|
||||
application_id=application_id,
|
||||
config_path=f"compose.services.{service_name}.enabled",
|
||||
strict=False,
|
||||
default=default,
|
||||
skip_missing_app=False,
|
||||
)
|
||||
shared = get_app_conf(
|
||||
applications=applications,
|
||||
application_id=application_id,
|
||||
config_path=f"compose.services.{service_name}.shared",
|
||||
strict=False,
|
||||
default=default,
|
||||
skip_missing_app=False,
|
||||
)
|
||||
return bool(enabled) and bool(shared)
|
||||
|
||||
|
||||
def _run_once_var_name(service_id: str) -> str:
|
||||
return f"run_once_{service_id.replace('-', '_')}"
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
"""
|
||||
Evaluate whether a backend service should be loaded.
|
||||
|
||||
Usage:
|
||||
{{ query('service_should_load', service_id, application_id=application_id, service_name=service_name) | first }}
|
||||
"""
|
||||
|
||||
def run(
|
||||
self,
|
||||
terms: List[Any],
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
) -> List[bool]:
|
||||
if not terms:
|
||||
return []
|
||||
|
||||
vars_ = variables or getattr(self._templar, "available_variables", {}) or {}
|
||||
applications = kwargs.get("applications", vars_.get("applications"))
|
||||
if not isinstance(applications, dict):
|
||||
raise AnsibleError(
|
||||
"service_should_load: required variable 'applications' must be a mapping"
|
||||
)
|
||||
|
||||
application_id = _require_non_empty(
|
||||
kwargs.get("application_id", vars_.get("application_id")), "application_id"
|
||||
)
|
||||
service_name = _require_non_empty(
|
||||
kwargs.get("service_name", vars_.get("service_name")), "service_name"
|
||||
)
|
||||
default = bool(kwargs.get("default", False))
|
||||
|
||||
results: List[bool] = []
|
||||
for term in terms:
|
||||
service_id = _require_non_empty(term, "service_id")
|
||||
run_once_defined = _run_once_var_name(service_id) in vars_
|
||||
should_load = (
|
||||
_service_enabled_and_shared(
|
||||
applications=applications,
|
||||
application_id=application_id,
|
||||
service_name=service_name,
|
||||
default=default,
|
||||
)
|
||||
and (not run_once_defined)
|
||||
and (application_id != service_id)
|
||||
)
|
||||
results.append(bool(should_load))
|
||||
|
||||
return results
|
||||
@@ -2,13 +2,14 @@
|
||||
ansible.builtin.set_fact:
|
||||
load_service: >-
|
||||
{{
|
||||
(applications | service_load_enabled(application_id, service_name, False))
|
||||
and
|
||||
(
|
||||
('run_once_' ~ (service_id | replace('-', '_'))) not in vars
|
||||
)
|
||||
and
|
||||
(application_id != service_id)
|
||||
query(
|
||||
'service_should_load',
|
||||
service_id,
|
||||
application_id=application_id,
|
||||
service_name=service_name,
|
||||
applications=applications,
|
||||
default=False
|
||||
) | first
|
||||
}}
|
||||
|
||||
- name: "SYS-STK-BACKEND | Load '{{ service_id }}' (Once)"
|
||||
|
||||
@@ -96,6 +96,73 @@
|
||||
- "--- compose logs {{ MATOMO_BOOTSTRAP_SERVICE }} stderr ---"
|
||||
- "{{ matomo_bootstrap_service_logs.stderr | default('') }}"
|
||||
|
||||
- name: "DEBUG | matomo container health (inspect json)"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- container
|
||||
- inspect
|
||||
- --format
|
||||
- '{{ "{{" }}json .State.Health{{ "}}" }}'
|
||||
- "{{ MATOMO_NAME }}"
|
||||
register: matomo_container_health
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: "DEBUG | matomo container health output"
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "--- matomo .State.Health (json) ---"
|
||||
- "{{ matomo_container_health.stdout | default('') }}"
|
||||
- "{{ matomo_container_health.stderr | default('') }}"
|
||||
|
||||
- name: "DEBUG | matomo tmp permissions + write test (www-data)"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- container
|
||||
- exec
|
||||
- --user
|
||||
- root
|
||||
- "{{ MATOMO_NAME }}"
|
||||
- sh
|
||||
- -lc
|
||||
- |
|
||||
set -eu
|
||||
id
|
||||
for p in /var/www/html /var/www/html/tmp /var/www/html/tmp/templates_c /var/www/html/config; do
|
||||
echo "===== PATH: ${p} ====="
|
||||
ls -ld "${p}" || true
|
||||
done
|
||||
echo "===== WRITE TEST AS www-data ====="
|
||||
su -s /bin/sh www-data -c "mkdir -p /var/www/html/tmp/templates_c && touch /var/www/html/tmp/templates_c/.write-test && rm -f /var/www/html/tmp/templates_c/.write-test"
|
||||
register: matomo_fs_permissions_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: "DEBUG | matomo tmp permissions + write test output"
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "--- filesystem + write-test diagnostics ---"
|
||||
- "{{ matomo_fs_permissions_check.stdout | default('') }}"
|
||||
- "{{ matomo_fs_permissions_check.stderr | default('') }}"
|
||||
|
||||
- name: "DEBUG | bootstrap artifact paths from logs"
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "--- bootstrap installer artifacts (paths from logs) ---"
|
||||
- >-
|
||||
{{
|
||||
(
|
||||
(
|
||||
matomo_bootstrap_service_logs.stdout | default('')
|
||||
) ~ '\n' ~ (
|
||||
matomo_bootstrap_service_logs.stderr | default('')
|
||||
)
|
||||
)
|
||||
| regex_findall('/tmp/matomo-bootstrap/installer-failure-[^\\s"]+\\.(?:png|html|txt)')
|
||||
| unique
|
||||
| list
|
||||
}}
|
||||
|
||||
- name: "DEBUG | show compose file"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
|
||||
@@ -158,8 +158,8 @@ echo ">>> PASS 2: re-init inventory (ASYNC_ENABLED=true)"
|
||||
--inventory-dir "${INVENTORY_DIR}" \
|
||||
--vars '{"ASYNC_ENABLED": true}'
|
||||
|
||||
echo ">>> PASS 2: deploy"
|
||||
"${PYTHON}" -m cli.deploy.development deploy "${deploy_args[@]}"
|
||||
echo ">>> PASS 2: deploy (skip wrapper cleanup)"
|
||||
"${PYTHON}" -m cli.deploy.development deploy "${deploy_args[@]}" -- --skip-cleanup
|
||||
|
||||
echo ">>> DISK / DOCKER STATE AFTER DEPLOY (before cleanup, distro=${INFINITO_DISTRO})"
|
||||
df -h || true
|
||||
|
||||
@@ -42,10 +42,12 @@
|
||||
- name: "Sanity check: all group_names must exist in applications"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item in applications
|
||||
fail_msg: "Group '{{ item }}' has no entry in 'applications'"
|
||||
success_msg: "Group '{{ item }}' is defined in 'applications'"
|
||||
- group_name in applications
|
||||
fail_msg: "Group '{{ group_name }}' has no entry in 'applications'"
|
||||
success_msg: "Group '{{ group_name }}' is defined in 'applications'"
|
||||
loop: "{{ group_names }}"
|
||||
loop_control:
|
||||
loop_var: group_name
|
||||
when: MODE_ASSERT | bool
|
||||
|
||||
- name: Merge current play applications
|
||||
@@ -183,7 +185,7 @@
|
||||
- DOCKER_IN_CONTAINER | bool
|
||||
|
||||
- name: "Load base roles"
|
||||
ansible.builtin.include_tasks: "./tasks/groups/{{ item }}-roles.yml"
|
||||
ansible.builtin.include_tasks: "./tasks/groups/{{ role_group }}-roles.yml"
|
||||
loop:
|
||||
- update # 1. Update system
|
||||
- drv # 2. Load driver roles
|
||||
@@ -194,7 +196,8 @@
|
||||
- svc-ai # 7. Load AI roles
|
||||
- svc-bkp # 8. Load Backup Roles
|
||||
loop_control:
|
||||
label: "{{ item }}-roles.yml"
|
||||
loop_var: role_group
|
||||
label: "{{ role_group }}-roles.yml"
|
||||
|
||||
- name: Install inventory-defined system packages
|
||||
ansible.builtin.include_role:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
- name: Setup server base
|
||||
include_role:
|
||||
name: "{{ item }}"
|
||||
name: "{{ server_role }}"
|
||||
loop:
|
||||
- user-administrator
|
||||
- user-root
|
||||
@@ -11,15 +11,18 @@
|
||||
- sys-ctl-cln-disc-space
|
||||
- sys-ctl-hlth-btrfs
|
||||
- sys-ctl-rpr-btrfs-balancer
|
||||
loop_control:
|
||||
loop_var: server_role
|
||||
|
||||
- name: "Include server roles"
|
||||
include_tasks: "./tasks/groups/{{ item }}-roles.yml"
|
||||
include_tasks: "./tasks/groups/{{ server_group }}-roles.yml"
|
||||
loop:
|
||||
- web-svc # 1. Load web server services
|
||||
- web-app # 2. Load (UI) web applications
|
||||
- web-opt # 4. Load optimation scrips last
|
||||
loop_control:
|
||||
label: "{{ item }}-roles.yml"
|
||||
loop_var: server_group
|
||||
label: "{{ server_group }}-roles.yml"
|
||||
|
||||
- name: Run E2E Cypress tests
|
||||
include_role:
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
- name: "Load destruction roles"
|
||||
include_tasks: "./tasks/groups/{{ item }}-roles.yml"
|
||||
include_tasks: "./tasks/groups/{{ destruction_group }}-roles.yml"
|
||||
loop:
|
||||
- svc-opt # Load optimation services
|
||||
loop_control:
|
||||
label: "{{ item }}-roles.yml"
|
||||
loop_var: destruction_group
|
||||
label: "{{ destruction_group }}-roles.yml"
|
||||
|
||||
- name: "Finalize services"
|
||||
include_role:
|
||||
|
||||
@@ -24,9 +24,9 @@ class TestMetaRolesIntegration(unittest.TestCase):
|
||||
for path in STAGES_DIR.rglob("*.yml"):
|
||||
cls.playbook_contents[path] = path.read_text(encoding="utf-8")
|
||||
|
||||
# Regex for include_tasks line with {{ item }}-roles.yml
|
||||
# Regex for include_tasks line with {{ <loop_var> }}-roles.yml
|
||||
cls.include_pattern = re.compile(
|
||||
r'include_tasks:\s*["\']\./tasks/groups/\{\{\s*item\s*\}\}-roles\.yml["\']'
|
||||
r'include_tasks:\s*["\']\./tasks/groups/\{\{\s*[A-Za-z_][A-Za-z0-9_]*\s*\}\}-roles\.yml["\']'
|
||||
)
|
||||
|
||||
def test_all_role_files_exist(self):
|
||||
|
||||
@@ -40,7 +40,9 @@ class TestCspTogglesDashboardLogout(unittest.TestCase):
|
||||
self._set_service_enabled(apps, "dashboard", True)
|
||||
header = self.filter.build_csp_header(apps, "app1", domains, "https")
|
||||
self.assertIn("frame-ancestors", header)
|
||||
self.assertIn("domain-example.com", header)
|
||||
frame_ancestors = self._get_directive_tokens(header, "frame-ancestors")
|
||||
self.assertIn("domain-example.com", frame_ancestors)
|
||||
self.assertNotIn("https://domain-example.com", frame_ancestors)
|
||||
|
||||
self._set_service_enabled(apps, "dashboard", False)
|
||||
header_no = self.filter.build_csp_header(apps, "app1", domains, "https")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import importlib.util
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
|
||||
def _repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[5]
|
||||
|
||||
|
||||
def _load_module(rel_path: str, name: str):
|
||||
path = _repo_root() / rel_path
|
||||
spec = importlib.util.spec_from_file_location(name, str(path))
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
class TestServiceShouldLoadLookup(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.mod = _load_module(
|
||||
"roles/sys-stk-backend/lookup_plugins/service_should_load.py",
|
||||
"service_should_load",
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.applications = {
|
||||
"web-app-test": {
|
||||
"compose": {
|
||||
"services": {
|
||||
"ldap": {"enabled": True, "shared": True},
|
||||
"ollama": {"enabled": True, "shared": False},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_true_when_enabled_shared_not_run_once_and_not_self(self):
|
||||
result = self.mod.LookupModule().run(
|
||||
["svc-db-openldap"],
|
||||
variables={
|
||||
"applications": self.applications,
|
||||
"application_id": "web-app-test",
|
||||
"service_name": "ldap",
|
||||
},
|
||||
)
|
||||
self.assertEqual(result, [True])
|
||||
|
||||
def test_false_when_run_once_already_defined(self):
|
||||
result = self.mod.LookupModule().run(
|
||||
["svc-db-openldap"],
|
||||
variables={
|
||||
"applications": self.applications,
|
||||
"application_id": "web-app-test",
|
||||
"service_name": "ldap",
|
||||
"run_once_svc_db_openldap": True,
|
||||
},
|
||||
)
|
||||
self.assertEqual(result, [False])
|
||||
|
||||
def test_false_when_not_shared(self):
|
||||
result = self.mod.LookupModule().run(
|
||||
["svc-ai-ollama"],
|
||||
variables={
|
||||
"applications": self.applications,
|
||||
"application_id": "web-app-test",
|
||||
"service_name": "ollama",
|
||||
},
|
||||
)
|
||||
self.assertEqual(result, [False])
|
||||
|
||||
def test_false_when_application_equals_service(self):
|
||||
result = self.mod.LookupModule().run(
|
||||
["web-app-test"],
|
||||
variables={
|
||||
"applications": self.applications,
|
||||
"application_id": "web-app-test",
|
||||
"service_name": "ldap",
|
||||
},
|
||||
)
|
||||
self.assertEqual(result, [False])
|
||||
|
||||
def test_raises_when_service_id_is_empty(self):
|
||||
with self.assertRaises(AnsibleError):
|
||||
self.mod.LookupModule().run(
|
||||
[" "],
|
||||
variables={
|
||||
"applications": self.applications,
|
||||
"application_id": "web-app-test",
|
||||
"service_name": "ldap",
|
||||
},
|
||||
)
|
||||
|
||||
def test_raises_when_applications_missing(self):
|
||||
with self.assertRaises(AnsibleError):
|
||||
self.mod.LookupModule().run(
|
||||
["svc-db-openldap"],
|
||||
variables={"application_id": "web-app-test", "service_name": "ldap"},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user