mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-09 04:06:42 +00:00
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:
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 }}"
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user