From f468633d1b32e8ef524b0cd8c73940598e3529b5 Mon Sep 17 00:00:00 2001 From: Jake Brower Date: Thu, 1 Oct 2026 08:47:58 -0500 Subject: [PATCH] feat(OMN-20306): a second principal issuer gives each developer machine its own CI-bus login for lab work Lab work runs on the CI bus, whose only lab-work login is a shared principal handed over by hand. The same issuer, a second instance on the CI bus network, now issues each machine its own principal with a caller-only grant set: write the unit request, read its terminals and the capacity ads, its own client group prefix, and cluster IdempotentWrite. The issuer accepts that one cluster grant and nothing broader. Onboarding facts declare its URL; the script asks only once it answers. Co-Authored-By: Claude Opus 5.5 --- deploy/lab/developer-onboarding.yaml | 7 + .../developer-principal-grants.ci-bus.yaml | 64 +++++ deploy/lane-census/lane-manifest.yaml | 17 ++ docker/Dockerfile.principal-issuer | 4 +- ...docker-compose.principal-issuer-ci-bus.yml | 64 +++++ scripts/generate_claude_lane_block.py | 8 + scripts/lab_principal_issuer.py | 43 ++- ...st_lab_principal_issuer_ci_bus_omn20306.py | 244 ++++++++++++++++++ .../test_lab_principal_issuer_omn20150.py | 9 + .../test_ci_bus_broker_isolation_omn18691.py | 40 ++- 10 files changed, 488 insertions(+), 12 deletions(-) create mode 100644 deploy/lab/developer-principal-grants.ci-bus.yaml create mode 100644 docker/docker-compose.principal-issuer-ci-bus.yml create mode 100644 tests/scripts/test_lab_principal_issuer_ci_bus_omn20306.py diff --git a/deploy/lab/developer-onboarding.yaml b/deploy/lab/developer-onboarding.yaml index 8803198cab..e76dd8576b 100644 --- a/deploy/lab/developer-onboarding.yaml +++ b/deploy/lab/developer-onboarding.yaml @@ -21,6 +21,13 @@ lane: dev # docker/docker-compose.principal-issuer.yml, published on the tailnet by the lab # host's `tailscale serve` (see that file's header). Tailnet-only. principal_issuer_url: https://omninode-pc.tail75df5e.ts.net:8443 +# The CI bus that lab work runs on (omnimarket config/ci_bus_lanes.yaml lane +# `ci-bus`), and the second issuer instance that gives a developer machine its +# own login there (docker/docker-compose.principal-issuer-ci-bus.yml, OMN-20306). +# The script requests this login only when the URL's /healthz answers, so it may +# be declared ahead of the deploy. +ci_bus_lane: ci-bus +ci_bus_issuer_url: https://omninode-pc.tail75df5e.ts.net:8444 # The lab model server, by its tailnet name so it answers on and off the office # LAN (local-dev walk D3). It serves the model id omnimarket's local rungs declare. lab_model_url: http://omnipc2.tail75df5e.ts.net:8000/v1/chat/completions diff --git a/deploy/lab/developer-principal-grants.ci-bus.yaml b/deploy/lab/developer-principal-grants.ci-bus.yaml new file mode 100644 index 0000000000..f1a154a799 --- /dev/null +++ b/deploy/lab/developer-principal-grants.ci-bus.yaml @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. +# SPDX-License-Identifier: MIT +# +# What a developer machine's CI-bus principal may do (OMN-20306). +# +# Lab work runs on the CI bus (docker/docker-compose.ci-bus.yml), not the dev +# lane. A developer machine sends a unit there with `onex lab-work run` (the omni +# plugin's `onex_lab_run.py --via bus`) and waits for its receipt. Until now the +# only login for that was the shared `ci-bus-labwork` principal (OMN-20213), +# handed over by hand. A second issuer instance +# (docker/docker-compose.principal-issuer-ci-bus.yml) applies exactly these +# grants to each developer machine instead. +# +# THE MINIMUM FOR ONE LAB-WORK CALL (omnimarket lab_work/bus.py LabWorkCaller): +# * produce the unit request; +# * read the capacity advertisements, to place the unit, and its two terminals; +# * read under the caller's own consumer-group prefix, +# derive_service_group_id("lab_work_client", service="omnimarket") under the +# env token `local`, plus a random suffix per call; +# * IdempotentWrite on the cluster: EventBusKafka producers enable idempotence +# by default, and producer-id allocation needs it (as OMN-20213 found for the +# shared login). It reaches no topic. +# +# DELIBERATELY NOT GRANTED, though the shared lab-work login holds them: +# * write on the terminals or the capacity topic (only a pool host publishes them); +# * the pool hosts' groups (local.omnimarket.node_lab_work_unit_effect...); +# * focused test runs and push validation; +# * any CI topic. +# +# RULES the issuer enforces on this file are the same as the dev lane's +# (developer-principal-grants.yaml), plus: a cluster grant carries +# idempotent_write only. +schema: developer-principal-grants.v1 +lane: ci-bus +# The same - name the dev lane issues: one machine, one +# name, on each broker that knows it. +principal_prefix: dev- +max_devices_per_login: 5 +allowed_login_domains: [] +grants: + - resource: topic + name: onex.cmd.omnimarket.lab-work-unit-requested.v1 + pattern: literal + operations: [write, describe] + - resource: topic + name: onex.evt.omnimarket.lab-work-unit-completed.v1 + pattern: literal + operations: [read, describe] + - resource: topic + name: onex.evt.omnimarket.lab-work-unit-failed.v1 + pattern: literal + operations: [read, describe] + - resource: topic + name: onex.evt.omnimarket.lab-host-capacity-advertised.v1 + pattern: literal + operations: [read, describe] + - resource: group + name: local.omnimarket.lab_work_client.consume.v1. + pattern: prefixed + operations: [read, describe] + - resource: cluster + name: kafka-cluster + pattern: literal + operations: [idempotent_write] diff --git a/deploy/lane-census/lane-manifest.yaml b/deploy/lane-census/lane-manifest.yaml index b8c08d5bb3..95827b0a92 100644 --- a/deploy/lane-census/lane-manifest.yaml +++ b/deploy/lane-census/lane-manifest.yaml @@ -637,6 +637,23 @@ lanes: kind: service replicas: 1 # --------------------------------------------------------------------------- + # OMN-20306: the CI-bus developer principal issuer. The same issuer, a second + # instance: issues each developer machine its own CI-bus login for lab work, + # from deploy/lab/developer-principal-grants.ci-bus.yaml. It joins the CI bus's + # network to reach that broker. `optional: true`: DECLARED-AHEAD-OF-STATE until + # the operator's first deploy. + principal-issuer-ci-bus: + hosts: [lab-201] + compose_file: docker/docker-compose.principal-issuer-ci-bus.yml + compose_project: omnibase-infra-principal-issuer-ci-bus + network: omninode-ci-bus-network + image_tag_pattern: '.+' + optional: true + services: + - name: omnibase-infra-principal-issuer-ci-bus + kind: service + replicas: 1 + # --------------------------------------------------------------------------- # Prospective OMN-18693 dogfood boundary: owner Codex; optional lane only. # No production or runtime evidence is asserted here; it is not deployed. # verified: 2026-09-25T01:20Z via read-only `docker ps -a --filter diff --git a/docker/Dockerfile.principal-issuer b/docker/Dockerfile.principal-issuer index 4f1de87ea9..7e692ba8ee 100644 --- a/docker/Dockerfile.principal-issuer +++ b/docker/Dockerfile.principal-issuer @@ -37,9 +37,11 @@ RUN set -eu; \ COPY scripts/__init__.py /app/scripts/__init__.py COPY scripts/lab_principal_issuer.py /app/scripts/lab_principal_issuer.py COPY deploy/lab/developer-principal-grants.yaml /app/developer-principal-grants.yaml +COPY deploy/lab/developer-principal-grants.ci-bus.yaml /app/developer-principal-grants.ci-bus.yaml USER issuer WORKDIR /app -RUN python3 scripts/lab_principal_issuer.py check --grants /app/developer-principal-grants.yaml +RUN python3 scripts/lab_principal_issuer.py check --grants /app/developer-principal-grants.yaml && \ + python3 scripts/lab_principal_issuer.py check --grants /app/developer-principal-grants.ci-bus.yaml ENTRYPOINT ["python3", "scripts/lab_principal_issuer.py"] CMD ["serve", "--grants", "/app/developer-principal-grants.yaml", "--ledger", "/data/issuances.jsonl", "--socket", "/run/principal-issuer/issuer.sock", "--proxy-uid", "0"] diff --git a/docker/docker-compose.principal-issuer-ci-bus.yml b/docker/docker-compose.principal-issuer-ci-bus.yml new file mode 100644 index 0000000000..602ac42955 --- /dev/null +++ b/docker/docker-compose.principal-issuer-ci-bus.yml @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. +# SPDX-License-Identifier: MIT +# +# OMN-20306: issues each developer machine its own CI-bus login, for lab work. +# +# The second instance of the principal issuer (docker-compose.principal-issuer.yml +# is the dev lane's). Same image, same code. It differs in four things: +# * the grants file is developer-principal-grants.ci-bus.yaml; +# * it joins the CI bus's network, to reach that broker's admin API and Kafka +# listener; +# * it authenticates as the CI bus's superuser (CI_BUS_KAFKA_SASL_*), from the +# same operator env file the CI bus's one-shots read; +# * it has its own ledger and socket volumes, published on its own tailscale +# serve port. +# +# Like the dev lane's issuer, it has no TCP listener and publishes no port. It +# trusts the tailnet login header only from the kernel-attested root peer +# (tailscale serve). Deploy it once, on the lab host, after the CI bus is up with +# SASL on: +# +# docker compose -f docker/docker-compose.principal-issuer-ci-bus.yml \ +# --env-file up -d --build +# tailscale serve --bg --https=8444 "unix:$(docker volume inspect -f '{{.Mountpoint}}' omnibase-infra-principal-issuer-ci-bus_principal-issuer-socket)/issuer.sock" +# +# The URL developer machines use is declared in deploy/lab/developer-onboarding.yaml +# (ci_bus_issuer_url). Onboarding skips the CI-bus login until that URL answers. +name: omnibase-infra-principal-issuer-ci-bus +services: + principal-issuer-ci-bus: + build: + context: .. + dockerfile: docker/Dockerfile.principal-issuer + container_name: omnibase-infra-principal-issuer-ci-bus + command: ["serve", "--grants", "/app/developer-principal-grants.ci-bus.yaml", "--ledger", "/data/issuances.jsonl", "--socket", "/run/principal-issuer/issuer.sock", "--proxy-uid", "0"] + environment: + RPK_USER: ${CI_BUS_KAFKA_SASL_USERNAME:?CI_BUS_KAFKA_SASL_USERNAME must be set - the issuer creates principals as the CI bus's superuser (OMN-18691); resolved from the operator env file, never inlined here} + RPK_PASS: ${CI_BUS_KAFKA_SASL_PASSWORD:?CI_BUS_KAFKA_SASL_PASSWORD must be set - the issuer creates principals as the CI bus's superuser (OMN-18691); resolved from the operator env file, never inlined here} + RPK_SASL_MECHANISM: SCRAM-SHA-256 + RPK_BROKERS: redpanda:9092 + RPK_ADMIN_HOSTS: redpanda:9644 + volumes: + - principal-issuer-ledger:/data + - principal-issuer-socket:/run/principal-issuer + networks: + - ci-bus + restart: unless-stopped + read_only: true + healthcheck: + test: ["CMD", "python3", "scripts/lab_principal_issuer.py", "health", "--socket", "/run/principal-issuer/issuer.sock"] + interval: 30s + timeout: 5s + retries: 3 + labels: + - "com.omninode.service=principal-issuer-ci-bus" + - "com.omninode.layer=infrastructure" + - "com.omninode.ticket=OMN-20306" +volumes: + principal-issuer-ledger: + principal-issuer-socket: +networks: + ci-bus: + # The network docker/docker-compose.ci-bus.yml declares for the CI bus's broker. + name: omninode-ci-bus-network + external: true diff --git a/scripts/generate_claude_lane_block.py b/scripts/generate_claude_lane_block.py index 98b2c76b9f..73b948bc50 100644 --- a/scripts/generate_claude_lane_block.py +++ b/scripts/generate_claude_lane_block.py @@ -150,6 +150,9 @@ # not a runtime lane: it has no runtime main/effects pair. It publishes no # port and serves a Unix socket reached only by the host's tailscale serve. "principal-issuer": _PORTS_NONE, + # OMN-20306 — the same issuer for the CI bus: a credential service, no + # runtime pair, no published port; a Unix socket behind tailscale serve. + "principal-issuer-ci-bus": _PORTS_NONE, } _LANE_BOUNDARY: dict[str, str] = { @@ -224,6 +227,11 @@ "network, unix socket only, no published port; NOT a runtime lane, " "never a proof lane, and never sourced for stability/prod grants" ), + "principal-issuer-ci-bus": ( + "developer CI-bus login issuer — a credential service on the .201 CI bus " + "network, unix socket only, no published port; NOT a runtime lane, " + "never a proof lane, and never sourced for stability/prod grants" + ), } diff --git a/scripts/lab_principal_issuer.py b/scripts/lab_principal_issuer.py index 04a04abf24..bc0a6ace70 100644 --- a/scripts/lab_principal_issuer.py +++ b/scripts/lab_principal_issuer.py @@ -24,8 +24,11 @@ broker superuser password. WHAT IT GRANTS - Exactly the grants in ``deploy/lab/developer-principal-grants.yaml``, which - this module validates before serving (see :func:`load_declaration`). A + Exactly the grants in the file passed as ``--grants``, which this module + validates before serving (see :func:`load_declaration`). One instance serves + one lane: ``deploy/lab/developer-principal-grants.yaml`` for the dev lane, and + ``deploy/lab/developer-principal-grants.ci-bus.yaml`` for the CI bus that lab + work runs on (OMN-20306), each with its own socket, ledger and broker. A principal is named ``-``; a login may hold at most ``max_devices_per_login`` of them. Asking again for the same device ROTATES its password: the password is never stored here, so re-issue is the only way @@ -83,7 +86,12 @@ PRINCIPAL_MAX_LEN = 63 MIN_PREFIXED_READ_GROUP_LEN = 16 ALLOWED_OPERATIONS = frozenset({"read", "write", "describe"}) -ALLOWED_RESOURCES = frozenset({"topic", "group"}) +ALLOWED_RESOURCES = frozenset({"topic", "group", "cluster"}) +# A Kafka producer with idempotence on (the EventBusKafka default) must allocate a +# producer id, which needs IdempotentWrite on the cluster. That grant reaches no +# topic, so it is the one cluster grant a declaration may carry (OMN-20306). +CLUSTER_NAME = "kafka-cluster" +CLUSTER_OPERATIONS = ("idempotent_write",) ALLOWED_PATTERNS = frozenset({"literal", "prefixed"}) @@ -172,6 +180,20 @@ def _grant(path: Path, index: int, raw: object) -> Grant: ) if not isinstance(name, str) or not name: raise DeclarationError(f"{where}: name must be a non-empty string") + if resource == "cluster": + if ( + name != CLUSTER_NAME + or pattern != "literal" + or not isinstance(operations, list) + or tuple(sorted(set(operations))) != CLUSTER_OPERATIONS + ): + raise DeclarationError( + f"{where}: a cluster grant is name {CLUSTER_NAME}, pattern literal and " + f"operations [idempotent_write] only (it reaches no topic)" + ) + return Grant( + resource=resource, name=name, pattern=pattern, operations=CLUSTER_OPERATIONS + ) if pattern not in ALLOWED_PATTERNS: raise DeclarationError( f"{where}: pattern must be one of {sorted(ALLOWED_PATTERNS)}" @@ -284,12 +306,15 @@ def grant(self, principal: str, grant: Grant) -> None: f"User:{principal}", ] argv += ["--operation", ",".join(grant.operations)] - argv += [ - f"--{grant.resource}", - grant.name, - "--resource-pattern-type", - grant.pattern, - ] + if grant.resource == "cluster": + argv += ["--cluster"] + else: + argv += [ + f"--{grant.resource}", + grant.name, + "--resource-pattern-type", + grant.pattern, + ] result = self._run(argv) if result.returncode != 0: raise IssuerError( diff --git a/tests/scripts/test_lab_principal_issuer_ci_bus_omn20306.py b/tests/scripts/test_lab_principal_issuer_ci_bus_omn20306.py new file mode 100644 index 0000000000..cd9a2890a6 --- /dev/null +++ b/tests/scripts/test_lab_principal_issuer_ci_bus_omn20306.py @@ -0,0 +1,244 @@ +# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. +# SPDX-License-Identifier: MIT +"""OMN-20306: a developer machine gets its own CI-bus login for lab work. + +The second issuer instance is the same code with the CI bus's grants file, its +compose file and the onboarding facts that point at it. These cover what the +grants allow, what they leave out, and that the instance is wired to the CI bus. +The broker is a fake: no network, no Docker. +""" + +from __future__ import annotations + +import re +import subprocess +from collections.abc import Sequence +from pathlib import Path +from typing import Any, cast + +import pytest +import yaml + +from scripts import lab_principal_issuer as lpi + +REPO_ROOT = Path(__file__).resolve().parents[2] +CI_GRANTS = REPO_ROOT / "deploy" / "lab" / "developer-principal-grants.ci-bus.yaml" +DEV_GRANTS = REPO_ROOT / "deploy" / "lab" / "developer-principal-grants.yaml" +FACTS = REPO_ROOT / "deploy" / "lab" / "developer-onboarding.yaml" +COMPOSE = "docker/docker-compose.principal-issuer-ci-bus.yml" +LAB_WORK_REQUEST = "onex.cmd.omnimarket.lab-work-unit-requested.v1" + +pytestmark = pytest.mark.unit + + +def _load_yaml(relative_path: str) -> dict[str, Any]: + return cast( + "dict[str, Any]", + yaml.safe_load((REPO_ROOT / relative_path).read_text(encoding="utf-8")), + ) + + +def _facts() -> dict[str, str]: + keys: dict[str, str] = {} + for line in FACTS.read_text().splitlines(): + if not line.strip() or line.lstrip().startswith("#"): + continue + match = re.fullmatch(r"([a-z_]+):[ \t]*(\S.*)", line) + assert match, f"not a flat key: value line: {line!r}" + keys[match.group(1)] = match.group(2) + return keys + + +@pytest.fixture +def declaration() -> lpi.Declaration: + return lpi.load_declaration(CI_GRANTS) + + +# --- the grants ---------------------------------------------------------------- + + +def test_the_ci_bus_grant_file_is_valid_and_for_the_ci_bus( + declaration: lpi.Declaration, +) -> None: + assert declaration.lane == "ci-bus" + assert lpi.main(["check", "--grants", str(CI_GRANTS)]) == 0 + + +def test_only_the_unit_request_is_writable(declaration: lpi.Declaration) -> None: + """A caller sends units; only a pool host publishes receipts or capacity.""" + writable = [g for g in declaration.grants if "write" in g.operations] + assert [(g.resource, g.name, g.pattern) for g in writable] == [ + ("topic", LAB_WORK_REQUEST, "literal") + ] + + +def test_the_caller_reads_its_terminals_and_the_capacity_ads( + declaration: lpi.Declaration, +) -> None: + readable = { + g.name + for g in declaration.grants + if g.resource == "topic" and "read" in g.operations + } + assert readable == { + "onex.evt.omnimarket.lab-work-unit-completed.v1", + "onex.evt.omnimarket.lab-work-unit-failed.v1", + "onex.evt.omnimarket.lab-host-capacity-advertised.v1", + } + + +def test_no_pool_host_group_focused_test_push_validation_or_ci_topic( + declaration: lpi.Declaration, +) -> None: + names = " ".join(g.name for g in declaration.grants) + for absent in ( + "node_lab_work_unit_effect", + "focused-test-run", + "node_focused_test_run_effect", + "push-validation", + "onex.evt.github.", + ): + assert absent not in names, absent + (group,) = [g for g in declaration.grants if g.resource == "group"] + assert group.name == "local.omnimarket.lab_work_client.consume.v1." + assert group.pattern == "prefixed" + assert group.operations == ("describe", "read") + + +def test_the_one_cluster_grant_is_idempotent_write( + declaration: lpi.Declaration, +) -> None: + clusters = [g for g in declaration.grants if g.resource == "cluster"] + assert [(g.name, g.operations) for g in clusters] == [ + ("kafka-cluster", ("idempotent_write",)) + ] + + +def test_a_cluster_grant_reaches_rpk_as_the_cluster_flag( + declaration: lpi.Declaration, +) -> None: + seen: list[list[str]] = [] + + def run(argv: Sequence[str]) -> subprocess.CompletedProcess[str]: + seen.append(list(argv)) + return subprocess.CompletedProcess(argv, 0, "", "") + + broker = lpi.Broker(lpi.BrokerConfig("http://admin:9644", "su", "su-pass"), run=run) + (cluster,) = [g for g in declaration.grants if g.resource == "cluster"] + broker.grant("dev-a-b", cluster) + assert seen == [ + [ + "rpk", + "security", + "acl", + "create", + "--allow-principal", + "User:dev-a-b", + "--operation", + "idempotent_write", + "--cluster", + ] + ] + + +@pytest.mark.parametrize( + "grant", + [ + { + "resource": "cluster", + "name": "kafka-cluster", + "pattern": "literal", + "operations": ["alter"], + }, + { + "resource": "cluster", + "name": "kafka-cluster", + "pattern": "literal", + "operations": ["idempotent_write", "describe"], + }, + { + "resource": "cluster", + "name": "other", + "pattern": "literal", + "operations": ["idempotent_write"], + }, + { + "resource": "cluster", + "name": "kafka-cluster", + "pattern": "prefixed", + "operations": ["idempotent_write"], + }, + { + "resource": "topic", + "name": "t", + "pattern": "literal", + "operations": ["idempotent_write"], + }, + ], +) +def test_a_cluster_grant_carries_idempotent_write_and_nothing_else( + tmp_path: Path, grant: dict[str, object] +) -> None: + doc = yaml.safe_load(CI_GRANTS.read_text()) + doc["grants"] = [grant] + path = tmp_path / "grants.yaml" + path.write_text(yaml.safe_dump(doc)) + with pytest.raises(lpi.DeclarationError): + lpi.load_declaration(path) + + +def test_the_dev_lane_grants_are_unchanged_by_the_cluster_rule() -> None: + dev = lpi.load_declaration(DEV_GRANTS) + assert dev.lane == "dev" + assert not [g for g in dev.grants if g.resource == "cluster"] + + +# --- the deployment ------------------------------------------------------------ + + +def test_the_instance_joins_the_ci_bus_network() -> None: + issuer = _load_yaml(COMPOSE) + ci_bus = _load_yaml("docker/docker-compose.ci-bus.yml") + (network_key,) = ci_bus["services"]["redpanda"]["networks"] + assert ( + issuer["networks"]["ci-bus"]["name"] == ci_bus["networks"][network_key]["name"] + ) + manifest = _load_yaml("deploy/lane-census/lane-manifest.yaml")["lanes"] + row = manifest["principal-issuer-ci-bus"] + assert row["network"] == issuer["networks"]["ci-bus"]["name"] + assert row["compose_file"] == COMPOSE + assert row["compose_project"] == issuer["name"] + + +def test_the_instance_serves_the_ci_bus_grants_on_a_unix_socket_only() -> None: + compose = _load_yaml(COMPOSE) + service = compose["services"]["principal-issuer-ci-bus"] + assert "ports" not in service + assert "expose" not in service + command = service["command"] + assert ( + command[command.index("--grants") + 1] + == "/app/developer-principal-grants.ci-bus.yaml" + ) + assert command[command.index("--socket") + 1] == "/run/principal-issuer/issuer.sock" + assert command[command.index("--proxy-uid") + 1] == "0" + dockerfile = (REPO_ROOT / "docker/Dockerfile.principal-issuer").read_text() + assert ( + "developer-principal-grants.ci-bus.yaml /app/developer-principal-grants.ci-bus.yaml" + in dockerfile + ) + assert "check --grants /app/developer-principal-grants.ci-bus.yaml" in dockerfile + + +def test_the_instance_authenticates_as_the_ci_bus_superuser_not_the_dev_lanes() -> None: + env = _load_yaml(COMPOSE)["services"]["principal-issuer-ci-bus"]["environment"] + assert env["RPK_USER"].startswith("${CI_BUS_KAFKA_SASL_USERNAME:?") + assert env["RPK_PASS"].startswith("${CI_BUS_KAFKA_SASL_PASSWORD:?") + assert "DEV_KAFKA" not in " ".join(str(v) for v in env.values()) + + +def test_the_onboarding_facts_name_the_ci_bus_issuer() -> None: + facts = _facts() + assert facts["ci_bus_lane"] == yaml.safe_load(CI_GRANTS.read_text())["lane"] + assert facts["ci_bus_issuer_url"].startswith("https://") + assert facts["ci_bus_issuer_url"] != facts["principal_issuer_url"] diff --git a/tests/scripts/test_lab_principal_issuer_omn20150.py b/tests/scripts/test_lab_principal_issuer_omn20150.py index 823e8490e4..40a6c6bdcb 100644 --- a/tests/scripts/test_lab_principal_issuer_omn20150.py +++ b/tests/scripts/test_lab_principal_issuer_omn20150.py @@ -149,6 +149,15 @@ def test_only_the_delegate_command_and_its_terminals_are_writable( ( { "resource": "cluster", + "name": "kafka-cluster", + "pattern": "literal", + "operations": ["describe"], + }, + "a cluster grant", + ), + ( + { + "resource": "transactional_id", "name": "k", "pattern": "literal", "operations": ["describe"], diff --git a/tests/unit/docker/test_ci_bus_broker_isolation_omn18691.py b/tests/unit/docker/test_ci_bus_broker_isolation_omn18691.py index fa7e8ecfec..2576a57e74 100644 --- a/tests/unit/docker/test_ci_bus_broker_isolation_omn18691.py +++ b/tests/unit/docker/test_ci_bus_broker_isolation_omn18691.py @@ -115,15 +115,51 @@ def lane_manifest() -> dict[str, Any]: return _load(LANE_MANIFEST) +# Services that join the CI bus's network as a GUEST: their compose file declares +# it `external: true`, so their `down` cannot remove it and the broker's fate is +# not coupled to theirs. They are not lanes whose network the bus must avoid. +CI_BUS_NETWORK_GUESTS = { + # OMN-20306: issues developer machines their CI-bus logins. + "principal-issuer-ci-bus": "docker/docker-compose.principal-issuer-ci-bus.yml", +} + + def _lane_specs(lane_manifest: dict[str, Any]) -> dict[str, dict[str, Any]]: - """Every manifest lane EXCEPT the CI bus entry itself.""" + """Every manifest lane EXCEPT the CI bus entry itself and its network guests.""" lanes = lane_manifest.get("lanes") assert isinstance(lanes, dict) and lanes, "lane manifest declares no lanes" return { name: spec for name, spec in lanes.items() - if name != CI_BUS_LANE_KEY and isinstance(spec, dict) + if name != CI_BUS_LANE_KEY + and name not in CI_BUS_NETWORK_GUESTS + and isinstance(spec, dict) + } + + +@pytest.mark.parametrize( + ("lane", "compose_file"), sorted(CI_BUS_NETWORK_GUESTS.items()) +) +def test_a_ci_bus_network_guest_only_attaches_to_it( + ci_bus: dict[str, Any], lane_manifest: dict[str, Any], lane: str, compose_file: str +) -> None: + """A guest may share the bus's network only as an external one it cannot remove.""" + spec = lane_manifest["lanes"][lane] + assert spec["compose_file"] == compose_file + guest = yaml.safe_load((REPO_ROOT / compose_file).read_text(encoding="utf-8")) + bus_networks = { + str(net["name"]) + for net in (ci_bus.get("networks") or {}).values() + if isinstance(net, dict) } + for net in (guest.get("networks") or {}).values(): + if net.get("name") in bus_networks: + assert net.get("external") is True, ( + f"{compose_file} must declare {net['name']} external" + ) + assert not any("ports" in svc for svc in (guest.get("services") or {}).values()), ( + f"{compose_file} publishes a port" + ) def _published_ports(compose: dict[str, Any]) -> set[str]: