feat(lint): let a variant declare it cannot carry the onion

services.tor.enabled pinned to a literal false on a role was the only incapacity the rule knew, and it is role-wide. web-app-opencloud serves the onion with Keycloak and cannot serve it without, because the built-in IDP it falls back to refuses a non-https issuer, and no service pin expresses a distinction that holds per variant.

A variant now marks its own tor pin '# nocheck: variant-tor-off <why>', the placement test_variant_tor_collateral already uses. The two helpers that locate that line, variant_spans and pin_line, move into the package beside role_dirs and load_role_meta rather than being copied a fourth time; the sibling reads them from there.

OpenCloud's SSO-less variants take the escape. libregraph/lico rejects a non-https iss unconditionally in bootstrap/bootstrap.go, IDP_INSECURE only relaxes outbound TLS, and IDP_ISS cannot point anywhere valid because an onion-only deployment has no https address, so those rounds crash-looped and answered 502. tor_capable reads the literal false and stops scheduling them behind the onion.

The variants-coverage marker on the role's tor service goes with it: it existed because the false polarity had nowhere to live, and test_coverage passes without it now. This supersedes the workaround 24d0308995 applied to this role, which pinned the onion back on for want of an escape, and restores what the file's own comment claimed throughout: variant 0 carries the onion.

Negative control: with the marker removed the rule reports both variants, with it eleven tests pass, covering the five tor lints, coverage, bond baseline and both budget checks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-10-21 00:21:51 +02:00
1 parent 396c937033
commit ed6e7a15fa
5 files changed
+62 -38

No files matched your search

@@ -76,7 +76,6 @@ prometheus:
shared: "{{ 'web-app-prometheus' in group_names }}"
# nocheck: playwright-service-flag
# nocheck: variants-coverage this role pairs with no service that refuses the onion, so variant-tor-off-needs-a-reason keeps the onion on in every variant and the false polarity has nowhere to live
tor:
bond: 0.5
enabled: "{{ 'svc-net-tor' in group_names }}"
+2 -2
View File
@@ -67,7 +67,7 @@
prometheus:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
tor:
tor: # nocheck: variant-tor-off without Keycloak OpenCloud runs its built-in lico IDP, which refuses a non-https issuer, and an onion has no TLS
enabled: false
- services:
@@ -98,5 +98,5 @@
prometheus:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
tor:
tor: # nocheck: variant-tor-off without Keycloak OpenCloud runs its built-in lico IDP, which refuses a non-https issuer, and an onion has no TLS
enabled: false
+33 -1
View File
@@ -9,7 +9,14 @@ from utils.cache.yaml import load_yaml_str
PROJECT_ROOT: Path = Path(__file__).resolve().parents[5]
ROLES_DIR: Path = PROJECT_ROOT / "roles"
__all__ = ["PROJECT_ROOT", "ROLES_DIR", "load_role_meta", "role_dirs"]
__all__ = [
"PROJECT_ROOT",
"ROLES_DIR",
"load_role_meta",
"pin_line",
"role_dirs",
"variant_spans",
]
def role_dirs() -> list[Path]:
@@ -17,6 +24,31 @@ def role_dirs() -> list[Path]:
return sorted(path for path in ROLES_DIR.iterdir() if path.is_dir())
def variant_spans(lines: list[str]) -> list[range]:
"""The line range each top-level list entry occupies.
Args:
lines: the variants file's lines.
"""
starts = [n for n, line in enumerate(lines, 1) if line.startswith("- ")]
bounds = [*starts[1:], len(lines) + 1]
return [range(start, end) for start, end in zip(starts, bounds, strict=True)]
def pin_line(lines: list[str], span: range, key: str) -> int | None:
"""The line declaring *key* inside one variant entry.
Args:
lines: the variants file's lines.
span: the variant's line range.
key: the service key to locate.
"""
for number in span:
if lines[number - 1].strip().startswith(f"{key}:"):
return number
return None
def load_role_meta(path: Path) -> Any:
"""Parse one role meta file.
@@ -34,7 +34,7 @@ from utils.roles.applications.services.registry import (
)
from utils.roles.mapping import ROLE_FILE_META_SERVICES, ROLE_FILE_META_VARIANTS
from . import ROLES_DIR, load_role_meta, role_dirs
from . import ROLES_DIR, load_role_meta, pin_line, role_dirs, variant_spans
RULE = "variant-tor-collateral"
TOR_KEY = "tor"
@@ -51,31 +51,6 @@ def _incapable_roles() -> set[str]:
return found
def _variant_spans(lines: list[str]) -> list[range]:
"""The line range each top-level list entry occupies.
Args:
lines: the variants file's lines.
"""
starts = [n for n, line in enumerate(lines, 1) if line.startswith("- ")]
bounds = [*starts[1:], len(lines) + 1]
return [range(start, end) for start, end in zip(starts, bounds, strict=True)]
def _pin_line(lines: list[str], span: range, key: str) -> int | None:
"""The line declaring *key* inside one variant entry.
Args:
lines: the variants file's lines.
span: the variant's line range.
key: the service key to locate.
"""
for number in span:
if lines[number - 1].strip().startswith(f"{key}:"):
return number
return None
def _collateral(
services: dict, role: str, registry: dict, incapable: set[str]
) -> list[str]:
@@ -113,7 +88,7 @@ class TestVariantTorCollateral(unittest.TestCase):
if not path.is_file():
continue
lines = read_text(str(path)).splitlines()
spans = _variant_spans(lines)
spans = variant_spans(lines)
for index, variant in enumerate(variants):
services = variant.get("services") or {}
if (services.get(TOR_KEY) or {}).get("enabled") is not False:
@@ -123,7 +98,7 @@ class TestVariantTorCollateral(unittest.TestCase):
collateral = _collateral(services, role, registry, incapable)
if not collateral:
continue
number = _pin_line(lines, spans[index], TOR_KEY)
number = pin_line(lines, spans[index], TOR_KEY)
if number is None or not is_suppressed_at(lines, number, RULE):
offenders.append(
f"{role} variant {index}: drops the onion while pinning "
@@ -16,20 +16,29 @@ Scope: a variant is excused when its own role is one of the incapable ones, or
when it pins a service whose provider role is. A role coupled only through
``group_names`` is invisible here, which is why the failure message asks for a
pin rather than assuming the author is wrong.
A role can also be incapable only in some of its variants, which no service
pin expresses: web-app-opencloud serves the onion with Keycloak and cannot
serve it without, because its built-in IDP refuses a non-https issuer. Such a
variant marks its own ``tor`` pin with ``# nocheck: variant-tor-off``, the
same placement :mod:`test_variant_tor_collateral` uses.
"""
from __future__ import annotations
import unittest
from utils.annotations.suppress import is_suppressed_at
from utils.cache.applications import get_variants
from utils.cache.files import read_text
from utils.roles.applications.services.registry import (
build_service_registry_from_roles_dir,
)
from utils.roles.mapping import ROLE_FILE_META_SERVICES
from utils.roles.mapping import ROLE_FILE_META_SERVICES, ROLE_FILE_META_VARIANTS
from . import ROLES_DIR, load_role_meta, role_dirs
from . import ROLES_DIR, load_role_meta, pin_line, role_dirs, variant_spans
RULE = "variant-tor-off"
TOR_KEY = "tor"
@@ -63,6 +72,11 @@ class TestVariantTorOffNeedsAReason(unittest.TestCase):
for role, variants in sorted(get_variants().items()):
if role in incapable:
continue
path = ROLES_DIR / role / ROLE_FILE_META_VARIANTS
if not path.is_file():
continue
lines = read_text(str(path)).splitlines()
spans = variant_spans(lines)
for index, variant in enumerate(variants):
services = variant.get("services") or {}
if (services.get(TOR_KEY) or {}).get("enabled") is not False:
@@ -74,7 +88,10 @@ class TestVariantTorOffNeedsAReason(unittest.TestCase):
and entry.get("enabled") is True
and providers.get(key) in incapable
]
if not excused:
if excused:
continue
number = pin_line(lines, spans[index], TOR_KEY) if index < len(spans) else None
if number is None or not is_suppressed_at(lines, number, RULE):
offenders.append(
f"{role} variant {index}: pins tor.enabled false while "
"enabling nothing that refuses the onion"
@@ -83,9 +100,10 @@ class TestVariantTorOffNeedsAReason(unittest.TestCase):
if offenders:
self.fail(
f"{len(offenders)} variant(s) drop the onion for no reason. Either "
"pin tor.enabled true so the round keeps onion coverage, or pin the "
"service whose role refuses the onion so the drop is visible:\n"
+ "\n".join(offenders)
"pin tor.enabled true so the round keeps onion coverage, pin the "
"service whose role refuses the onion so the drop is visible, or "
f"mark the tor pin '# nocheck: {RULE} <why this variant cannot "
"carry one>':\n" + "\n".join(offenders)
)
def test_at_least_one_role_refuses_the_onion(self) -> None: