feat: add repository projection synchronization
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 1m9s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 1m9s
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a028f0-a42f-7582-89a8-ebaad7343834
This commit is contained in:
parent
d3258a6ba0
commit
280da08455
7 changed files with 317 additions and 2 deletions
102
src/sbom_nexus/projection.py
Normal file
102
src/sbom_nexus/projection.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
"""Synchronize the minimum repository projection used by SBOM Nexus."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
import urllib.parse
|
||||
from collections import Counter
|
||||
from typing import Any
|
||||
|
||||
from sbom_nexus.importer import request_json
|
||||
|
||||
|
||||
def _checkout_path(repo: dict[str, Any], host_id: str | None) -> str | None:
|
||||
host_paths = repo.get("host_paths") or {}
|
||||
selected_host = host_id or socket.gethostname()
|
||||
return host_paths.get(selected_host) or repo.get("local_path")
|
||||
|
||||
|
||||
def sync_repository_projections(
|
||||
source_url: str,
|
||||
target_url: str,
|
||||
*,
|
||||
dry_run: bool,
|
||||
host_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Upsert repository identity/path projections without scanning or ingesting."""
|
||||
repositories = request_json(source_url, "/repos/")
|
||||
projections: dict[str, dict[str, Any]] = {}
|
||||
statuses = Counter()
|
||||
for repo in repositories:
|
||||
slug = repo["slug"]
|
||||
if slug in projections:
|
||||
return {
|
||||
"ok": False,
|
||||
"dry_run": dry_run,
|
||||
"source_repo_count": len(repositories),
|
||||
"error": f"duplicate repository slug: {slug}",
|
||||
}
|
||||
active = repo.get("status", "active") == "active"
|
||||
checkout_path = _checkout_path(repo, host_id)
|
||||
projections[slug] = {
|
||||
"slug": slug,
|
||||
"active": active,
|
||||
"checkout_path": checkout_path,
|
||||
}
|
||||
statuses["active" if active else "inactive"] += 1
|
||||
statuses["with_checkout_path" if checkout_path else "without_checkout_path"] += 1
|
||||
|
||||
if dry_run:
|
||||
return {
|
||||
"ok": True,
|
||||
"dry_run": True,
|
||||
"host_id": host_id or socket.gethostname(),
|
||||
"source_repo_count": len(projections),
|
||||
"counts": dict(statuses),
|
||||
"would_upsert": len(projections),
|
||||
"reconciliation": None,
|
||||
}
|
||||
|
||||
for slug, projection in sorted(projections.items()):
|
||||
request_json(
|
||||
target_url,
|
||||
f"/repositories/{urllib.parse.quote(slug, safe='')}",
|
||||
method="PUT",
|
||||
body={
|
||||
"active": projection["active"],
|
||||
"checkout_path": projection["checkout_path"],
|
||||
},
|
||||
)
|
||||
|
||||
target_repositories = {
|
||||
repo["slug"]: repo for repo in request_json(target_url, "/repositories/")
|
||||
}
|
||||
missing = sorted(set(projections) - set(target_repositories))
|
||||
mismatched = []
|
||||
for slug in sorted(set(projections) & set(target_repositories)):
|
||||
expected = projections[slug]
|
||||
actual = target_repositories[slug]
|
||||
differences = {
|
||||
field: {"source": expected[field], "target": actual.get(field)}
|
||||
for field in ("active", "checkout_path")
|
||||
if expected[field] != actual.get(field)
|
||||
}
|
||||
if differences:
|
||||
mismatched.append({"repo_slug": slug, "differences": differences})
|
||||
|
||||
reconciliation = {
|
||||
"ok": not missing and not mismatched,
|
||||
"matched_repo_count": len(projections) - len(missing) - len(mismatched),
|
||||
"missing_repo_slugs": missing,
|
||||
"mismatched_repositories": mismatched,
|
||||
"extra_target_repo_slugs": sorted(set(target_repositories) - set(projections)),
|
||||
}
|
||||
return {
|
||||
"ok": reconciliation["ok"],
|
||||
"dry_run": False,
|
||||
"host_id": host_id or socket.gethostname(),
|
||||
"source_repo_count": len(projections),
|
||||
"counts": dict(statuses),
|
||||
"upserted": len(projections),
|
||||
"reconciliation": reconciliation,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue