Files
computer-playbook/tests/integration/test_compose_build_requires_image.py
T
kevinveenbirkenbach 53e817fa22 refactor(compose): migrate from docker-compose.yml to compose.yml everywhere
- Rename all docker-compose.yml templates to compose.yml
- Update override and CA override filenames accordingly
- Adjust compose wrapper, lookup plugins, filters and path helpers
- Update roles, tasks, handlers and scripts to use compose CLI
- Rewrite documentation and admin guides to reference compose
- Update unit and integration tests to expect compose.yml
- Keep internal variable keys for backward compatibility where needed

https://chatgpt.com/share/69870272-b168-800f-84c3-6417398e441f
2026-02-07 10:14:21 +01:00

156 lines
5.3 KiB
Python

import re
import unittest
from pathlib import Path
BUILD_LOOKUP_RE = re.compile(
r"""\{\{\s*lookup\(\s*['"]template['"]\s*,\s*['"]roles/sys-svc-container/templates/build\.yml\.j2['"]\s*\)\s*\|\s*indent\(\s*4\s*\)\s*\}\}"""
)
def image_key_re(indent: str) -> re.Pattern:
return re.compile(rf"^{re.escape(indent)}image\s*:")
def merge_key_re(indent: str) -> re.Pattern:
# YAML merge key: "<<:"
return re.compile(rf"^{re.escape(indent)}<<\s*:")
_JINJA_DIRECTIVE_RE = re.compile(r"^\s*\{[%#].*[%#]\}\s*$")
class TestComposeBuildTemplateRequiresImageTag(unittest.TestCase):
"""
Verify that every roles/*/templates/compose.yml.j2 which calls:
{{ lookup('template', 'roles/sys-svc-container/templates/build.yml.j2') | indent(4) }}
also defines either:
- an `image:` tag on the same indentation level, OR
- a YAML merge key `<<:` on the same indentation level (since it may inject `image:`).
"""
@staticmethod
def _repo_root() -> Path:
return Path(__file__).resolve().parents[2]
@staticmethod
def _iter_compose_templates(repo_root: Path):
roles_dir = repo_root / "roles"
if not roles_dir.is_dir():
return []
return sorted(roles_dir.glob("*/templates/compose.yml.j2"))
@staticmethod
def _indent_len(line: str) -> int:
return len(line) - len(line.lstrip(" "))
@staticmethod
def _find_block_end(lines: list[str], start_idx: int, base_indent_len: int) -> int:
for i in range(start_idx + 1, len(lines)):
line = lines[i]
if not line.strip():
continue
if _JINJA_DIRECTIVE_RE.match(line):
continue
if (
TestComposeBuildTemplateRequiresImageTag._indent_len(line)
< base_indent_len
):
return i
return len(lines)
@staticmethod
def _find_service_header(
lines: list[str], from_idx: int, props_indent_len: int
) -> int | None:
for i in range(from_idx, -1, -1):
line = lines[i]
if not line.strip():
continue
if _JINJA_DIRECTIVE_RE.match(line):
continue
ind = TestComposeBuildTemplateRequiresImageTag._indent_len(line)
if ind < props_indent_len:
if line.rstrip().endswith(":") and not line.lstrip().startswith(
("-", "#")
):
return i
return None
def test_build_lookup_requires_image_or_merge_on_same_level(self):
repo_root = self._repo_root()
compose_files = self._iter_compose_templates(repo_root)
self.assertTrue(
compose_files,
f"No compose templates found under {(repo_root / 'roles').as_posix()}/ */templates/compose.yml.j2",
)
violations: list[str] = []
for path in compose_files:
text = path.read_text(encoding="utf-8")
if not BUILD_LOOKUP_RE.search(text):
continue
lines = text.splitlines()
checked_services: set[tuple[int, int]] = (
set()
) # (header_idx, props_indent_len)
for idx, line in enumerate(lines):
if not BUILD_LOOKUP_RE.search(line):
continue
props_indent = line[: len(line) - len(line.lstrip(" "))]
props_indent_len = len(props_indent)
header_idx = self._find_service_header(lines, idx, props_indent_len)
if header_idx is None:
rel = path.relative_to(repo_root)
violations.append(
f"{rel}:{idx + 1} -> build.yml.j2 lookup present, but could not determine the enclosing service block."
)
continue
key = (header_idx, props_indent_len)
if key in checked_services:
continue
checked_services.add(key)
service_base_indent_len = self._indent_len(lines[header_idx])
block_end = self._find_block_end(
lines, header_idx, service_base_indent_len
)
img_re = image_key_re(props_indent)
mrg_re = merge_key_re(props_indent)
# Search the whole service block (from header to end)
has_image = any(
img_re.match(lines[j]) for j in range(header_idx + 1, block_end)
)
has_merge = any(
mrg_re.match(lines[j]) for j in range(header_idx + 1, block_end)
)
if not (has_image or has_merge):
rel = path.relative_to(repo_root)
violations.append(
f"{rel}:{idx + 1} -> build.yml.j2 lookup present, but neither 'image:' nor '<<:' found at indent level {props_indent_len} in the same service block."
)
if violations:
msg = (
"Found compose.yml.j2 templates that include build.yml.j2 but miss 'image:' (or YAML merge '<<:') on the same level:\n"
+ "\n".join(f"- {v}" for v in violations)
)
self.fail(msg)
if __name__ == "__main__":
unittest.main(verbosity=2)