diff --git a/ci/scripts/hermes_webui_release.py b/ci/scripts/hermes_webui_release.py index 1a744508..be1ac8d3 100755 --- a/ci/scripts/hermes_webui_release.py +++ b/ci/scripts/hermes_webui_release.py @@ -17,6 +17,7 @@ from hermes_webui_flux_release import ( DEFAULT_IMAGE as DEFAULT_IMAGE, DESTINATION_PATTERN, DIGEST_PATTERN, + REVISION_PATTERN, render_workload as render_workload, validate_destination, validate_release_artifacts as validate_flux_release_artifacts, @@ -232,13 +233,17 @@ def assert_tag_absent( def verify_registry_digest( destination: str, digest: str, + source_revision: str, *, username: str, password: str, opener: Callable[[urllib.request.Request, int], Any] = _registry_request, ) -> None: - """Verify Harbor independently resolves the pushed tag to Kaniko's digest.""" + """Verify Harbor resolves the tag, digest, and persisted source revision.""" digest = _validated(digest, DIGEST_PATTERN, "image digest") + source_revision = _validated( + source_revision, REVISION_PATTERN, "source revision" + ) status, body = _artifact_response( destination, username=username, password=password, opener=opener ) @@ -263,6 +268,11 @@ def verify_registry_digest( raise RuntimeError("Harbor artifact does not contain the expected tag") if matching_tags[0].get("immutable") is not True: raise RuntimeError("Harbor did not enforce the expected tag as immutable") + labels = ((artifact.get("extra_attrs") or {}).get("config") or {}).get("Labels") + if not isinstance(labels, dict): + raise RuntimeError("Harbor artifact omitted OCI image labels") + if labels.get("org.opencontainers.image.revision") != source_revision: + raise RuntimeError("Harbor OCI source-revision label does not match") def validate_release_artifacts( @@ -356,7 +366,11 @@ def main() -> int: destination=args.destination, ) verify_registry_digest( - args.destination, digest, username=username, password=password + args.destination, + digest, + args.source_revision, + username=username, + password=password, ) write_release_artifacts( digest=digest, diff --git a/testing/tests/test_hermes_webui_release.py b/testing/tests/test_hermes_webui_release.py index e9fb0765..3e77d118 100644 --- a/testing/tests/test_hermes_webui_release.py +++ b/testing/tests/test_hermes_webui_release.py @@ -232,12 +232,24 @@ def test_release_verifies_exact_webui_harbor_artifact_and_policy() -> None: { "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, username="robot", password="private", opener=artifact_open + destination, + digest, + revision, + username="robot", + password="private", + opener=artifact_open, ) request = seen[0][0] assert "/repositories/hermes-webui/artifacts/" in request.full_url @@ -283,6 +295,37 @@ def test_release_verifies_exact_webui_harbor_artifact_and_policy() -> None: ) +@pytest.mark.parametrize("labels", [None, {}, {"org.opencontainers.image.revision": "c" * 40}]) +def test_release_rejects_missing_or_wrong_harbor_source_revision(labels) -> None: + """A tag derived from Git cannot substitute for the persisted OCI label.""" + module = _load(RELEASE, f"webui_registry_revision_{labels!r}") + revision = "a" * 40 + destination = f"{module.DEFAULT_IMAGE}:git-{revision}-build-9" + digest = "sha256:" + "b" * 64 + + def artifact_open(_request, _timeout): + config = {} if labels is None else {"Labels": labels} + return _Response( + json.dumps( + { + "digest": digest, + "tags": [{"name": f"git-{revision}-build-9", "immutable": True}], + "extra_attrs": {"config": config}, + } + ).encode() + ) + + with pytest.raises(RuntimeError, match="OCI (image labels|source-revision label)"): + module.verify_registry_digest( + destination, + digest, + revision, + username="robot", + password="private", + opener=artifact_open, + ) + + def test_flux_tracks_webui_policy_before_jenkins() -> None: """The immutable Harbor rule is reviewed desired state, not a pipeline wish.""" harbor = yaml.safe_load(