Add registry artifacts and projections

This commit is contained in:
2026-05-17 20:31:12 +02:00
parent 2523eecce2
commit faff5fb728
6 changed files with 402 additions and 8 deletions

View File

@@ -50,6 +50,27 @@ class RegistryStore:
create index if not exists idx_snapshots_repo_latest
on snapshots(repo_slug, id desc);
create table if not exists artifacts (
id integer primary key autoincrement,
repo_slug text not null references repositories(slug),
target_id text,
target_kind text,
artifact_type text not null,
name text not null,
uri text not null,
media_type text,
digest text,
version text,
metadata_json text not null,
created_at text not null
);
create index if not exists idx_artifacts_repo
on artifacts(repo_slug);
create index if not exists idx_artifacts_target
on artifacts(target_id);
"""
)
@@ -201,6 +222,100 @@ class RegistryStore:
"edges": sorted(edges, key=lambda edge: (edge["from"], edge["to"], edge["type"])),
}
def add_artifact(self, payload: dict[str, Any]) -> dict[str, Any]:
repo_slug = _required_text(payload, "repo_slug")
self.get_repository(repo_slug)
artifact_type = _required_text(payload, "artifact_type", fallback_key="type")
uri = _required_text(payload, "uri")
name = str(payload.get("name") or uri)
target_id = _optional_text(payload, "target_id")
target_kind = _optional_text(payload, "target_kind")
media_type = _optional_text(payload, "media_type")
digest = _optional_text(payload, "digest")
version = _optional_text(payload, "version")
metadata = payload.get("metadata", {})
if not isinstance(metadata, dict):
raise RegistryError("field 'metadata' must be an object")
now = _utc_now()
with self._connect() as db:
cursor = db.execute(
"""
insert into artifacts (
repo_slug, target_id, target_kind, artifact_type, name, uri,
media_type, digest, version, metadata_json, created_at
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
repo_slug,
target_id,
target_kind,
artifact_type,
name,
uri,
media_type,
digest,
version,
json.dumps(metadata, sort_keys=True),
now,
),
)
artifact_id = int(cursor.lastrowid)
return self.get_artifact(artifact_id)
def list_artifacts(
self,
repo_slug: str | None = None,
target_id: str | None = None,
artifact_type: str | None = None,
) -> list[dict[str, Any]]:
conditions: list[str] = []
params: list[str] = []
if repo_slug:
conditions.append("repo_slug = ?")
params.append(repo_slug)
if target_id:
conditions.append("target_id = ?")
params.append(target_id)
if artifact_type:
conditions.append("artifact_type = ?")
params.append(artifact_type)
where = f" where {' and '.join(conditions)}" if conditions else ""
with self._connect() as db:
rows = db.execute(
"""
select id, repo_slug, target_id, target_kind, artifact_type, name, uri,
media_type, digest, version, metadata_json, created_at
from artifacts
"""
+ where
+ " order by repo_slug, target_id, artifact_type, id",
params,
).fetchall()
return [_artifact_dict(row) for row in rows]
def get_artifact(self, artifact_id: int) -> dict[str, Any]:
with self._connect() as db:
row = db.execute(
"""
select id, repo_slug, target_id, target_kind, artifact_type, name, uri,
media_type, digest, version, metadata_json, created_at
from artifacts
where id = ?
""",
(artifact_id,),
).fetchone()
if row is None:
raise RegistryError(f"artifact not found: {artifact_id}", 404)
return _artifact_dict(row)
def graph_node_detail(self, graph_id: str) -> dict[str, Any]:
node = graph_node(self.combined_graph(), graph_id)
enriched = json.loads(json.dumps(node))
enriched["artifacts"] = self.list_artifacts(target_id=graph_id)
return enriched
def _connect(self) -> sqlite3.Connection:
db = sqlite3.connect(self.path)
db.row_factory = sqlite3.Row
@@ -382,6 +497,123 @@ def graph_node(graph: dict[str, Any], graph_id: str) -> dict[str, Any]:
return node
def backstage_projection(graph: dict[str, Any]) -> dict[str, Any]:
items: list[dict[str, Any]] = []
domains = sorted({str(node.get("domain", "")) for node in _nodes(graph) if node.get("domain")})
for domain in domains:
items.append(
{
"apiVersion": "backstage.io/v1alpha1",
"kind": "Domain",
"metadata": {"name": _backstage_name(domain), "title": domain},
"spec": {"owner": _backstage_name(domain)},
}
)
items.append(
{
"apiVersion": "backstage.io/v1alpha1",
"kind": "System",
"metadata": {"name": _backstage_name(domain), "title": domain},
"spec": {"owner": _backstage_name(domain), "domain": _backstage_name(domain)},
}
)
for node in _nodes(graph):
kind = str(node.get("kind", ""))
if kind == "ServiceDeclaration":
attrs = _attrs(node)
items.append(
{
"apiVersion": "backstage.io/v1alpha1",
"kind": "Component",
"metadata": _backstage_metadata(node),
"spec": {
"type": "service",
"lifecycle": _backstage_lifecycle(str(node.get("lifecycle", ""))),
"owner": _backstage_owner(node),
"system": _backstage_name(str(node.get("domain", ""))),
"providesApis": [_backstage_name(value) for value in attrs.get("exposes_interfaces", [])],
},
}
)
elif kind == "InterfaceDeclaration":
attrs = _attrs(node)
items.append(
{
"apiVersion": "backstage.io/v1alpha1",
"kind": "API",
"metadata": _backstage_metadata(node),
"spec": {
"type": _backstage_api_type(str(attrs.get("interface_type", ""))),
"lifecycle": _backstage_lifecycle(str(node.get("lifecycle", ""))),
"owner": _backstage_owner(node),
"system": _backstage_name(str(node.get("domain", ""))),
"definition": "",
},
}
)
elif kind == "CapabilityDeclaration":
attrs = _attrs(node)
items.append(
{
"apiVersion": "backstage.io/v1alpha1",
"kind": "Resource",
"metadata": _backstage_metadata(node),
"spec": {
"type": attrs.get("capability_type", "capability"),
"owner": _backstage_owner(node),
"system": _backstage_name(str(node.get("domain", ""))),
},
}
)
return {
"apiVersion": "railiance.fabric/v1alpha1",
"kind": "BackstageCatalogProjection",
"items": items,
}
def xregistry_projection(graph: dict[str, Any]) -> dict[str, Any]:
groups = {
"services": _xregistry_group("service", "services"),
"capabilities": _xregistry_group("capability", "capabilities"),
"interfaces": _xregistry_group("interface", "interfaces"),
"dependencies": _xregistry_group("dependency", "dependencies"),
"bindings": _xregistry_group("binding", "bindings"),
}
group_by_kind = {
"ServiceDeclaration": "services",
"CapabilityDeclaration": "capabilities",
"InterfaceDeclaration": "interfaces",
"DependencyDeclaration": "dependencies",
"BindingAssertion": "bindings",
}
for node in _nodes(graph):
group_name = group_by_kind.get(str(node.get("kind", "")))
if group_name is None:
continue
node_id = str(node.get("id", ""))
groups[group_name]["resources"][_xregistry_key(node_id)] = {
"id": node_id,
"name": node.get("name", node_id),
"versionid": "latest",
"attributes": {
"kind": node.get("kind", ""),
"repo": node.get("repo", ""),
"domain": node.get("domain", ""),
"lifecycle": node.get("lifecycle", ""),
**_attrs(node),
},
}
return {
"apiVersion": "railiance.fabric/v1alpha1",
"kind": "XRegistryProjection",
"groups": groups,
}
def _with_source(graph: dict[str, Any], repo_slug: str, commit: str, generated_at: str) -> dict[str, Any]:
copy = json.loads(json.dumps(graph))
copy.setdefault("generated_at", generated_at)
@@ -407,8 +639,27 @@ def _row_dict(row: sqlite3.Row) -> dict[str, Any]:
return {key: row[key] for key in row.keys()}
def _required_text(payload: dict[str, Any], key: str) -> str:
def _artifact_dict(row: sqlite3.Row) -> dict[str, Any]:
return {
"id": row["id"],
"repo_slug": row["repo_slug"],
"target_id": row["target_id"],
"target_kind": row["target_kind"],
"artifact_type": row["artifact_type"],
"name": row["name"],
"uri": row["uri"],
"media_type": row["media_type"],
"digest": row["digest"],
"version": row["version"],
"metadata": json.loads(row["metadata_json"]),
"created_at": row["created_at"],
}
def _required_text(payload: dict[str, Any], key: str, fallback_key: str | None = None) -> str:
value = payload.get(key)
if value is None and fallback_key is not None:
value = payload.get(fallback_key)
if not isinstance(value, str) or not value.strip():
raise RegistryError(f"field '{key}' is required")
return value.strip()
@@ -480,3 +731,57 @@ def _consumer_match(attrs: dict[str, Any], dependency_id: str, binding: dict[str
"provider_interface_id": binding.get("provider_interface_id", ""),
"status": binding.get("status", ""),
}
def _backstage_metadata(node: dict[str, Any]) -> dict[str, Any]:
node_id = str(node.get("id", ""))
metadata = {
"name": _backstage_name(node_id),
"title": node.get("name", node_id),
"annotations": {
"railiance.fabric/id": node_id,
"railiance.fabric/repo": str(node.get("repo", "")),
"railiance.fabric/kind": str(node.get("kind", "")),
},
}
return metadata
def _backstage_owner(node: dict[str, Any]) -> str:
repo = str(node.get("repo", ""))
domain = str(node.get("domain", ""))
return _backstage_name(repo or domain or "unknown")
def _backstage_name(value: str) -> str:
cleaned = "".join(char.lower() if char.isalnum() else "-" for char in value)
cleaned = "-".join(part for part in cleaned.split("-") if part)
return cleaned or "unknown"
def _backstage_lifecycle(value: str) -> str:
if value in {"active", "deprecated"}:
return value
if value == "retired":
return "deprecated"
return "experimental"
def _backstage_api_type(interface_type: str) -> str:
if interface_type == "http-api":
return "openapi"
if interface_type == "event-stream":
return "asyncapi"
return "other"
def _xregistry_group(singular: str, plural: str) -> dict[str, Any]:
return {
"singular": singular,
"plural": plural,
"resources": {},
}
def _xregistry_key(value: str) -> str:
return value.replace("/", ".").strip(".") or "unknown"