Prechádzať zdrojové kódy

fix(validation): allow raw downstream jinja syntax

ChristianLempa 1 týždeň pred
rodič
commit
a217a7ff8c

+ 0 - 23
cli/core/template/template.py

@@ -26,7 +26,6 @@ logger = logging.getLogger(__name__)
 TEMPLATE_MANIFEST_FILENAME = "template.json"
 LEGACY_TEMPLATE_FILENAMES = ("template.yaml", "template.yml")
 TEMPLATE_FILES_DIRNAME = "files"
-LEGACY_JINJA_DELIMITERS = ("{{", "{%", "{#")
 VARIABLE_START = "<<"
 VARIABLE_END = ">>"
 BLOCK_START = "<%"
@@ -351,34 +350,12 @@ class Template:
         template_files.sort(key=lambda item: str(item.relative_path))
         self.__template_files = template_files
 
-    def _validate_delimiters(self) -> None:
-        """Reject legacy Jinja delimiters in 0.2.0 templates."""
-        for template_file in self.template_files:
-            file_path = self.files_dir / template_file.relative_path
-            try:
-                content = file_path.read_text(encoding="utf-8")
-            except OSError as exc:
-                raise TemplateValidationError(
-                    f"Failed to read template file '{template_file.relative_path}': {exc}"
-                ) from exc
-
-            for delimiter in LEGACY_JINJA_DELIMITERS:
-                if delimiter in content:
-                    raise TemplateValidationError(
-                        f"Legacy Jinja delimiter '{delimiter}' found in '{template_file.relative_path}'. "
-                        f"Use {VARIABLE_START} {VARIABLE_END} for variables, "
-                        f"{BLOCK_START} {BLOCK_END} for blocks, and "
-                        f"{COMMENT_START} {COMMENT_END} for comments."
-                    )
-
     def _extract_all_used_variables(self) -> set[str]:
         """Extract undeclared variables from all files under files/."""
         used_variables: set[str] = set()
         syntax_errors = []
         self._variable_usage_map: dict[str, list[str]] = {}
 
-        self._validate_delimiters()
-
         for template_file in self.template_files:
             file_path = self.files_dir / template_file.relative_path
             try:

+ 18 - 1
cli/core/validation/kind_validators.py

@@ -229,6 +229,10 @@ class AnsibleValidator(RenderedFilesValidator):
             )
             failure = self.failure_from_process(process, str(playbook.relative_to(workdir)))
             if failure is not None:
+                if self._is_dependency_resolution_failure(failure.message):
+                    result.skipped = True
+                    result.warnings.append(failure.message)
+                    continue
                 result.failures.append(failure)
         return result
 
@@ -238,6 +242,19 @@ class AnsibleValidator(RenderedFilesValidator):
         for path in workdir.rglob("*"):
             if not path.is_file() or path.suffix.lower() not in {".yaml", ".yml"}:
                 continue
-            if "playbook" in path.name.lower():
+            if "playbook" in path.name.lower() or AnsibleValidator._looks_like_playbook(path):
                 candidates.append(path)
         return candidates
+
+    @staticmethod
+    def _looks_like_playbook(path: Path) -> bool:
+        try:
+            content = path.read_text(encoding="utf-8")
+        except OSError:
+            return False
+
+        return any(line.lstrip().startswith("hosts:") for line in content.splitlines())
+
+    @staticmethod
+    def _is_dependency_resolution_failure(message: str) -> bool:
+        return ("the role" in message and "was not found" in message) or "couldn't resolve module/action" in message

+ 20 - 1
tests/test_dependency_matrix.py

@@ -4,7 +4,13 @@ import json
 from pathlib import Path
 
 from cli.core.template import Template
-from cli.core.validation import DependencyMatrixBuilder, KindValidationResult, MatrixOptions, ValidationRunner
+from cli.core.validation import (
+    AnsibleValidator,
+    DependencyMatrixBuilder,
+    KindValidationResult,
+    MatrixOptions,
+    ValidationRunner,
+)
 
 
 def _write_template(tmp_path: Path, manifest: dict, files: dict[str, str]) -> Template:
@@ -161,3 +167,16 @@ def test_validation_runner_treats_unavailable_kind_validator_as_skip(tmp_path: P
     assert summary.kind_available is False
     assert summary.kind_skipped_cases == {"defaults"}
     assert summary.failures == []
+
+
+def test_ansible_validator_detects_main_yml_playbook_by_hosts_key(tmp_path: Path) -> None:
+    playbook = tmp_path / "main.yml"
+    playbook.write_text("- name: Configure host\n  hosts: all\n  tasks: []\n", encoding="utf-8")
+
+    assert AnsibleValidator._find_playbooks(tmp_path) == [playbook]
+
+
+def test_ansible_validator_classifies_missing_collection_as_dependency_resolution_failure() -> None:
+    assert AnsibleValidator._is_dependency_resolution_failure("ERROR! the role 'vendor.role' was not found")
+    assert AnsibleValidator._is_dependency_resolution_failure("ERROR! couldn't resolve module/action 'vendor.module'")
+    assert not AnsibleValidator._is_dependency_resolution_failure("ERROR! Syntax Error while loading YAML")

+ 25 - 13
tests/test_template.py

@@ -8,7 +8,7 @@ from pathlib import Path
 
 import pytest
 
-from cli.core.exceptions import TemplateLoadError, TemplateValidationError
+from cli.core.exceptions import TemplateLoadError
 from cli.core.library import Library
 from cli.core.template import Template, normalize_template_slug
 
@@ -244,17 +244,17 @@ def test_legacy_template_yaml_is_rejected(tmp_path: Path) -> None:
         Template(template_dir, library_name="default")
 
 
-def test_legacy_jinja_delimiters_are_rejected(tmp_path: Path) -> None:
-    """files/ content must use the new custom delimiters."""
-    template_dir = tmp_path / "compose" / "legacy-delimiters"
+def test_raw_jinja_delimiters_are_allowed_in_rendered_files(tmp_path: Path) -> None:
+    """Rendered files may contain raw Jinja syntax for downstream tools like Ansible."""
+    template_dir = tmp_path / "ansible" / "raw-jinja"
     _write_template_json(
         template_dir,
         {
-            "slug": "legacy-delimiters",
-            "kind": "compose",
+            "slug": "raw-jinja",
+            "kind": "ansible",
             "metadata": {
-                "name": "Legacy delimiters",
-                "description": "Bad template",
+                "name": "Raw Jinja",
+                "description": "Ansible template",
                 "version": {
                     "name": "v1.0.0",
                 },
@@ -265,23 +265,35 @@ def test_legacy_jinja_delimiters_are_rejected(tmp_path: Path) -> None:
                     "title": "General",
                     "items": [
                         {
-                            "name": "container_hostname",
+                            "name": "target_host",
                             "type": "str",
-                            "title": "Container hostname",
+                            "title": "Target host",
+                            "default": "all",
                         }
                     ],
                 }
             ],
         },
         {
-            "compose.yaml": "hostname: {{ container_hostname }}\n",
+            "playbook.yaml": (
+                "- hosts: << target_host >>\n"
+                "  tasks:\n"
+                "    - debug:\n"
+                '        msg: "{{ ansible_hostname }}"\n'
+                "    - name: Raw Ansible block delimiter remains literal\n"
+                "      debug:\n"
+                '        msg: "{% raw %}{{ value }}{% endraw %}"\n'
+            ),
         },
     )
 
     template = Template(template_dir, library_name="default")
+    rendered_files, _ = template.render(template.variables)
 
-    with pytest.raises(TemplateValidationError, match="Legacy Jinja delimiter"):
-        _ = template.used_variables
+    assert template.used_variables == {"target_host"}
+    assert "{{ ansible_hostname }}" in rendered_files["playbook.yaml"]
+    assert "{% raw %}{{ value }}{% endraw %}" in rendered_files["playbook.yaml"]
+    assert "- hosts: all" in rendered_files["playbook.yaml"]
 
 
 def test_template_json_rejects_string_metadata_version(tmp_path: Path) -> None: