417 lines
16 KiB
Python
417 lines
16 KiB
Python
"""Exact-source, Harbor, and Flux contracts for the chat-router release lane."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import urllib.parse
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
PIPELINE = ROOT / "ci/Jenkinsfile.hermes-chat-router-image"
|
|
RELEASE = ROOT / "ci/scripts/hermes_chat_router_release.py"
|
|
PROMOTE = ROOT / "ci/scripts/hermes_oci_promote.py"
|
|
MANIFEST = ROOT / "services/hermes/chat-router.yaml"
|
|
IMAGE_POLICY = ROOT / "services/hermes/image.yaml"
|
|
JENKINS = ROOT / "services/jenkins/configmap-jcasc.yaml"
|
|
TRIGGER = ROOT / "services/hermes/scripts/jenkins_image_build_trigger.py"
|
|
STATUS = ROOT / "services/hermes/scripts/hermes_image_release_status.py"
|
|
HARBOR_JOB = ROOT / "services/harbor/hermes-chat-router-immutability-job.yaml"
|
|
HARBOR_POLICY = (
|
|
ROOT / "services/harbor/scripts/harbor_hermes_chat_router_immutability_ensure.py"
|
|
)
|
|
HARBOR_GENERIC = ROOT / "services/harbor/scripts/harbor_immutable_rule_ensure.py"
|
|
|
|
|
|
def _load(path: Path, name: str):
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
assert spec is not None and spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
class Response(io.BytesIO):
|
|
"""Minimal context-managed urllib response."""
|
|
|
|
def __init__(self, status: int, body: bytes = b"", headers=None):
|
|
super().__init__(body)
|
|
self.status = status
|
|
self.headers = headers or {}
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_args):
|
|
self.close()
|
|
|
|
|
|
def _fixture(tmp_path: Path):
|
|
module = _load(RELEASE, f"router_release_{tmp_path.name}")
|
|
digest = "sha256:" + "7" * 64
|
|
revision = "8" * 40
|
|
build = "23"
|
|
destination = f"{module.DEFAULT_IMAGE}:git-{revision}-build-{build}"
|
|
digest_file = tmp_path / "digest"
|
|
image_file = tmp_path / "image"
|
|
digest_file.write_text(digest + "\n", encoding="utf-8")
|
|
image_file.write_text(f"{destination}@{digest}\n", encoding="utf-8")
|
|
output = tmp_path / "release"
|
|
kwargs = {
|
|
"digest": digest,
|
|
"source_revision": revision,
|
|
"build_number": build,
|
|
"destination": destination,
|
|
"manifest": MANIFEST,
|
|
"output_dir": output,
|
|
}
|
|
module.write_release_artifacts(**kwargs)
|
|
return module, digest_file, image_file, kwargs
|
|
|
|
|
|
def test_pipeline_builds_one_exact_reviewed_router_revision() -> None:
|
|
"""The job detaches the reviewed main ancestor and never mutates Git or K8s."""
|
|
source = PIPELINE.read_text(encoding="utf-8")
|
|
assert 'test "${CONFIRM_PUBLISH}" = "PUBLISH HERMES CHAT ROUTER"' in source
|
|
assert 'test "${main_revision}" = "$(git rev-parse origin/main)"' in source
|
|
assert 'git merge-base --is-ancestor "${EXPECTED_SOURCE_REVISION}"' in source
|
|
assert 'git checkout --detach "${EXPECTED_SOURCE_REVISION}"' in source
|
|
assert 'test "${actual_revision}" = "${EXPECTED_SOURCE_REVISION}"' in source
|
|
assert "dockerfiles/Dockerfile.hermes-chat-router" in source
|
|
assert "GO111MODULE=off CGO_ENABLED=0 go test ./..." in source
|
|
assert "ci/scripts/hermes_chat_router_release.py assert-absent" in source
|
|
assert "ci/scripts/hermes_oci_promote.py" in source
|
|
assert "--digest-file=" in source
|
|
assert "--image-name-tag-with-digest-file=" in source
|
|
for label in (
|
|
"org.opencontainers.image.revision=${source_revision}",
|
|
"org.opencontainers.image.source=https://scm.bstein.dev/titan/atlas-iac",
|
|
"org.opencontainers.image.title=hermes-chat-router",
|
|
):
|
|
assert label in source
|
|
for forbidden in ("kubectl ", "flux reconcile", "git push", "git commit"):
|
|
assert forbidden not in source
|
|
pod_yaml = source.split('yaml """', 1)[1].split('"""', 1)[0]
|
|
pod = yaml.safe_load(pod_yaml)["spec"]
|
|
assert pod["serviceAccountName"] == "hermes-image-builder"
|
|
assert pod["automountServiceAccountToken"] is False
|
|
assert pod["enableServiceLinks"] is False
|
|
containers = {item["name"]: item for item in pod["containers"]}
|
|
assert set(containers) == {"jnlp", "python", "golang", "kaniko"}
|
|
assert pod["affinity"]["nodeAffinity"][
|
|
"requiredDuringSchedulingIgnoredDuringExecution"
|
|
]["nodeSelectorTerms"][0]["matchExpressions"][0]["values"] == ["titan-20"]
|
|
assert "docker.sock" not in source and "hostPath" not in source
|
|
for container in containers.values():
|
|
security = container["securityContext"]
|
|
assert security["allowPrivilegeEscalation"] is False
|
|
assert security["capabilities"]["drop"] == ["ALL"]
|
|
|
|
|
|
def test_jenkins_job_is_token_guarded_main_only_and_non_concurrent() -> None:
|
|
"""Only the fixed router job and fixed confirmation can enter the lane."""
|
|
config = yaml.safe_load(JENKINS.read_text(encoding="utf-8"))
|
|
jobs = config["data"]["jobs.yaml"]
|
|
assert jobs.count("pipelineJob('hermes-chat-router-image')") == 1
|
|
block = jobs.split("pipelineJob('hermes-chat-router-image')", 1)[1].split(
|
|
"pipelineJob('hermes-voice-image')", 1
|
|
)[0]
|
|
assert "branches('*/main')" in block
|
|
assert "scriptPath('ci/Jenkinsfile.hermes-chat-router-image')" in block
|
|
assert "authenticationToken(System.getenv('HERMES_AGENT_IMAGE_BUILD_TOKEN'))" in block
|
|
assert "PUBLISH HERMES CHAT ROUTER" in block
|
|
assert "pipelineTriggers" not in block
|
|
pipeline = PIPELINE.read_text(encoding="utf-8")
|
|
assert "disableConcurrentBuilds()" in pipeline
|
|
assert "artifactDaysToKeepStr: '30'" in pipeline
|
|
|
|
|
|
def test_release_renders_exactly_one_flux_consumer_and_revalidates(tmp_path: Path) -> None:
|
|
"""A release handoff contains only the one router Deployment mutation."""
|
|
module, digest_file, image_file, kwargs = _fixture(tmp_path)
|
|
output = kwargs["output_dir"]
|
|
patch = (output / "hermes-chat-router-image-update.patch").read_text()
|
|
assert patch.count(f"+ image: {module.DEFAULT_IMAGE}@{kwargs['digest']}") == 1
|
|
assert "services/hermes/chat-router.yaml" in patch
|
|
assert "chat-statefulset.yaml" not in patch
|
|
metadata = json.loads((output / "hermes-chat-router-image.json").read_text())
|
|
assert metadata["source_revision"] == kwargs["source_revision"]
|
|
assert metadata["flux_targets"] == [
|
|
"apps/Deployment/hermes/hermes-chat-router"
|
|
]
|
|
module.validate_release_artifacts(**kwargs)
|
|
assert module.validate_kaniko_evidence(
|
|
digest_text=digest_file.read_text(),
|
|
image_text=image_file.read_text(),
|
|
destination=kwargs["destination"],
|
|
) == kwargs["digest"]
|
|
(output / "extra").write_text("no\n")
|
|
with pytest.raises(ValueError, match="exactly three"):
|
|
module.validate_release_artifacts(**kwargs)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"source",
|
|
[
|
|
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wrong\n",
|
|
"# services/hermes/chat-router.yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: hermes-chat-router\n",
|
|
],
|
|
)
|
|
def test_renderer_fails_closed_on_target_drift(source: str) -> None:
|
|
"""Identity or consumer-count drift cannot silently broaden promotion."""
|
|
module = _load(RELEASE, f"router_drift_{len(source)}")
|
|
with pytest.raises(ValueError):
|
|
module.render_workload(source, "sha256:" + "a" * 64)
|
|
|
|
|
|
def test_harbor_candidate_evidence_and_policy_are_exact() -> None:
|
|
"""Candidate acceptance requires an immutable tag, digest, and OCI revision."""
|
|
module = _load(RELEASE, "router_registry_contract")
|
|
revision = "a" * 40
|
|
digest = "sha256:" + "b" * 64
|
|
destination = f"{module.DEFAULT_IMAGE}:git-{revision}-build-9"
|
|
seen = []
|
|
|
|
def artifact_open(request, timeout):
|
|
seen.append((request, timeout))
|
|
return Response(
|
|
200,
|
|
json.dumps(
|
|
{
|
|
"digest": digest,
|
|
"tags": [
|
|
{"name": f"git-{revision}-build-9", "immutable": True}
|
|
],
|
|
"extra_attrs": {
|
|
"config": {
|
|
"Labels": {
|
|
"org.opencontainers.image.revision": revision
|
|
}
|
|
}
|
|
},
|
|
}
|
|
).encode(),
|
|
)
|
|
|
|
module.verify_registry_digest(
|
|
destination,
|
|
digest,
|
|
revision,
|
|
username="robot",
|
|
password="private",
|
|
opener=artifact_open,
|
|
)
|
|
assert "/repositories/hermes-chat-router/artifacts/" in seen[0][0].full_url
|
|
assert seen[0][0].get_header("Authorization").startswith("Basic ")
|
|
|
|
def rule_open(_request, _timeout):
|
|
expected = {
|
|
"disabled": False,
|
|
"action": "immutable",
|
|
"template": "immutable_template",
|
|
"tag_selectors": [
|
|
{
|
|
"kind": "doublestar",
|
|
"decoration": "matches",
|
|
"pattern": "git-*-build-*",
|
|
}
|
|
],
|
|
"scope_selectors": {
|
|
"repository": [
|
|
{
|
|
"kind": "doublestar",
|
|
"decoration": "repoMatches",
|
|
"pattern": "hermes-chat-router",
|
|
}
|
|
]
|
|
},
|
|
}
|
|
return Response(
|
|
200, json.dumps([expected]).encode(), {"X-Total-Count": "1"}
|
|
)
|
|
|
|
module.verify_immutable_policy(
|
|
username="robot", password="private", opener=rule_open
|
|
)
|
|
assert module.assert_tag_absent(
|
|
destination,
|
|
username="robot",
|
|
password="private",
|
|
opener=lambda *_args: Response(404),
|
|
) is None
|
|
with pytest.raises(RuntimeError, match="already exists"):
|
|
module.assert_tag_absent(
|
|
destination,
|
|
username="robot",
|
|
password="private",
|
|
opener=lambda *_args: Response(200),
|
|
)
|
|
|
|
|
|
def test_harbor_policy_is_flux_tracked_and_vault_injected() -> None:
|
|
"""The preflight rule is desired state and no credential is stored in Git."""
|
|
app = yaml.safe_load(
|
|
(ROOT / "clusters/atlas/flux-system/applications/harbor/kustomization.yaml")
|
|
.read_text(encoding="utf-8")
|
|
)
|
|
assert {
|
|
"apiVersion": "batch/v1",
|
|
"kind": "Job",
|
|
"name": "harbor-hermes-chat-router-immutability-ensure-1",
|
|
"namespace": "harbor",
|
|
} in app["spec"]["healthChecks"]
|
|
_load(HARBOR_GENERIC, "harbor_immutable_rule_ensure")
|
|
policy = _load(HARBOR_POLICY, "router_harbor_policy")
|
|
assert policy.REPOSITORY_PATTERN == "hermes-chat-router"
|
|
assert policy.TAG_PATTERN == "git-*-build-*"
|
|
job = yaml.safe_load(HARBOR_JOB.read_text(encoding="utf-8"))
|
|
template = job["spec"]["template"]
|
|
annotations = template["metadata"]["annotations"]
|
|
assert annotations["vault.hashicorp.com/role"] == "harbor-policy-bootstrap"
|
|
assert annotations[
|
|
"vault.hashicorp.com/agent-inject-secret-harbor-admin-password"
|
|
] == "kv/data/atlas/harbor/harbor-core"
|
|
container = template["spec"]["containers"][0]
|
|
assert "@sha256:" in container["image"]
|
|
assert container["securityContext"]["readOnlyRootFilesystem"] is True
|
|
assert container["securityContext"]["runAsNonRoot"] is True
|
|
source = HARBOR_POLICY.read_text().lower() + HARBOR_GENERIC.read_text().lower()
|
|
assert "private-token" not in source and "password123" not in source
|
|
|
|
|
|
def test_harbor_policy_helper_creates_once_and_rejects_weakened_rule() -> None:
|
|
"""Creation is verified by ID; a disabled matching rule fails closed."""
|
|
module = _load(HARBOR_GENERIC, "router_harbor_helper")
|
|
expected = module.expected_rule("hermes-chat-router", "git-*-build-*")
|
|
created = {**expected, "id": 19}
|
|
|
|
class Client:
|
|
origin = module.EXPECTED_ORIGIN
|
|
|
|
def __init__(self):
|
|
self.calls = 0
|
|
|
|
def request(self, method, path, payload=None):
|
|
self.calls += 1
|
|
if self.calls == 1:
|
|
return 200, b"[]", {"X-Total-Count": "0"}
|
|
if self.calls == 2:
|
|
assert method == "POST" and payload == expected
|
|
return 201, b"", {"Location": "/api/v2.0" + path + "/19"}
|
|
return 200, json.dumps([created]).encode(), {"X-Total-Count": "1"}
|
|
|
|
client = Client()
|
|
assert module.ensure_rule(
|
|
client,
|
|
project="bstein",
|
|
repository="hermes-chat-router",
|
|
tag_pattern="git-*-build-*",
|
|
) == 19
|
|
weakened = {**created, "disabled": True}
|
|
|
|
class Weakened:
|
|
origin = module.EXPECTED_ORIGIN
|
|
|
|
def request(self, *_args, **_kwargs):
|
|
return 200, json.dumps([weakened]).encode(), {"X-Total-Count": "1"}
|
|
|
|
with pytest.raises(RuntimeError, match="not enabled and exact"):
|
|
module.ensure_rule(
|
|
Weakened(),
|
|
project="bstein",
|
|
repository="hermes-chat-router",
|
|
tag_pattern="git-*-build-*",
|
|
)
|
|
|
|
|
|
def test_flux_policy_and_marker_select_only_router_releases() -> None:
|
|
"""Candidates remain invisible until the exact release suffix exists."""
|
|
documents = list(yaml.safe_load_all(IMAGE_POLICY.read_text(encoding="utf-8")))
|
|
repository = next(
|
|
item
|
|
for item in documents
|
|
if item["kind"] == "ImageRepository"
|
|
and item["metadata"]["name"] == "hermes-chat-router-release"
|
|
)
|
|
policy = next(
|
|
item
|
|
for item in documents
|
|
if item["kind"] == "ImagePolicy"
|
|
and item["metadata"]["name"] == "hermes-chat-router-release"
|
|
)
|
|
assert repository["spec"]["image"] == (
|
|
"registry.bstein.dev/bstein/hermes-chat-router"
|
|
)
|
|
assert policy["spec"]["filterTags"]["pattern"].endswith("-release$")
|
|
assert policy["spec"]["digestReflectionPolicy"] == "Always"
|
|
marker = '"$imagepolicy": "hermes:hermes-chat-router-release"'
|
|
assert MANIFEST.read_text(encoding="utf-8").count(marker) == 1
|
|
|
|
|
|
def test_runtime_trigger_and_status_support_only_fixed_router_contract(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Hermes can request and follow router delivery without choosing a job."""
|
|
trigger = _load(TRIGGER, "router_release_trigger")
|
|
token = tmp_path / "token"
|
|
token.write_text("private-token\n", encoding="utf-8")
|
|
captured = {}
|
|
|
|
def opener(request, timeout):
|
|
captured["request"] = request
|
|
assert timeout == 20
|
|
return Response(
|
|
201, headers={"Location": "https://ci.bstein.dev/queue/item/42/"}
|
|
)
|
|
|
|
revision = "c" * 40
|
|
result = trigger.trigger_build(
|
|
revision, component="router", token_file=token, opener=opener
|
|
)
|
|
fields = urllib.parse.parse_qs(captured["request"].data.decode())
|
|
assert fields["job"] == ["hermes-chat-router-image"]
|
|
assert fields["CONFIRM_PUBLISH"] == ["PUBLISH HERMES CHAT ROUTER"]
|
|
assert "private-token" not in json.dumps(result)
|
|
assert result["follow_command"].endswith(
|
|
f"--component router --revision {revision} --wait"
|
|
)
|
|
status = _load(STATUS, "router_release_status")
|
|
assert status.COMPONENTS["router"] == {
|
|
"policy": "hermes-chat-router-release",
|
|
"repository": "registry.bstein.dev/bstein/hermes-chat-router",
|
|
"workloads": (
|
|
("deployment", "hermes-chat-router", "hermes-chat-router"),
|
|
),
|
|
}
|
|
|
|
|
|
def test_generic_promoter_accepts_router_and_rejects_other_repositories() -> None:
|
|
"""The manifest copier allow-list adds only chat-router."""
|
|
module = _load(PROMOTE, "router_promote_contract")
|
|
revision = "d" * 40
|
|
digest = "sha256:" + "e" * 64
|
|
destination = (
|
|
"registry.bstein.dev/bstein/hermes-chat-router:"
|
|
f"git-{revision}-build-7"
|
|
)
|
|
component, tag, observed = module._validated_release(
|
|
destination, digest, revision, "7"
|
|
)
|
|
assert component == "hermes-chat-router"
|
|
assert tag == f"git-{revision}-build-7"
|
|
assert observed == digest
|
|
with pytest.raises(ValueError):
|
|
module._validated_release(
|
|
"registry.bstein.dev/bstein/not-router:git-" + revision + "-build-7",
|
|
digest,
|
|
revision,
|
|
"7",
|
|
)
|