provisioning-platform/crates/catalog-registry/src/handlers.rs

398 lines
10 KiB
Rust
Raw Permalink Normal View History

use std::sync::Arc;
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
routing::{get, head, post, put},
Json, Router,
};
use catalog_registry::service::{
ExtensionMetadata, CatalogRegistry, ImageManifest, PushBlobRequest,
};
use ontoref_derive::onto_api;
use serde_json::json;
/// Application state
#[derive(Clone)]
pub struct AppState {
registry: Arc<CatalogRegistry>,
}
impl AppState {
pub fn new(registry: Arc<CatalogRegistry>) -> Self {
Self { registry }
}
}
/// Error response
#[derive(Debug)]
pub struct RegistryError {
status: StatusCode,
message: String,
}
impl IntoResponse for RegistryError {
fn into_response(self) -> axum::response::Response {
let body = Json(json!({
"errors": [{
"code": "REGISTRY_ERROR",
"message": self.message,
}]
}));
(self.status, body).into_response()
}
}
impl RegistryError {
pub fn not_found(msg: impl Into<String>) -> Self {
Self {
status: StatusCode::NOT_FOUND,
message: msg.into(),
}
}
pub fn internal(msg: impl Into<String>) -> Self {
Self {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: msg.into(),
}
}
#[allow(dead_code)]
pub fn bad_request(msg: impl Into<String>) -> Self {
Self {
status: StatusCode::BAD_REQUEST,
message: msg.into(),
}
}
}
/// Check if blob exists (HEAD /v2/<name>/blobs/<digest>)
#[onto_api(
method = "HEAD",
path = "/v2/{name}/blobs/{digest}",
description = "Check if blob exists (OCI v2)",
auth = "bearer",
actors = "developer, agent",
tags = "oci, blobs",
feature = ""
)]
async fn blob_exists(
Path((_name, digest)): Path<(String, String)>,
State(state): State<AppState>,
) -> Result<StatusCode, RegistryError> {
state
.registry
.blob_exists(&digest)
.await
.map_err(|e: anyhow::Error| RegistryError::internal(e.to_string()))?;
Ok(StatusCode::OK)
}
/// Pull blob (GET /v2/<name>/blobs/<digest>)
#[onto_api(
method = "GET",
path = "/v2/{name}/blobs/{digest}",
description = "Pull blob content (OCI v2)",
auth = "bearer",
actors = "developer, agent",
tags = "oci, blobs",
feature = ""
)]
async fn pull_blob(
Path((_name, digest)): Path<(String, String)>,
State(state): State<AppState>,
) -> Result<Vec<u8>, RegistryError> {
state
.registry
.pull_blob(&digest)
.await
.map_err(|e: anyhow::Error| {
if e.to_string().contains("not found") {
RegistryError::not_found(e.to_string())
} else {
RegistryError::internal(e.to_string())
}
})
}
/// Push blob (POST /v2/<name>/blobs/uploads/)
#[onto_api(
method = "POST",
path = "/v2/{name}/blobs/uploads",
description = "Push blob content (OCI v2)",
auth = "bearer",
actors = "developer",
tags = "oci, blobs",
feature = ""
)]
async fn push_blob(
Path(_name): Path<String>,
State(state): State<AppState>,
Json(req): Json<PushBlobRequest>,
) -> Result<(StatusCode, Json<serde_json::Value>), RegistryError> {
let response = state
.registry
.push_blob(req)
.await
.map_err(|e: anyhow::Error| RegistryError::internal(e.to_string()))?;
Ok((
StatusCode::CREATED,
Json(json!({
"digest": response.digest,
"size": response.size,
})),
))
}
/// Pull manifest (GET /v2/<name>/manifests/<reference>)
#[onto_api(
method = "GET",
path = "/v2/{name}/manifests/{reference}",
description = "Pull extension manifest (OCI v2)",
auth = "bearer",
actors = "developer, agent",
tags = "oci, manifests",
feature = ""
)]
async fn pull_manifest(
Path((name, reference)): Path<(String, String)>,
State(state): State<AppState>,
) -> Result<(StatusCode, Json<serde_json::Value>), RegistryError> {
let response = state
.registry
.get_manifest(&format!("{}:{}", name, reference))
.await
.map_err(|e: anyhow::Error| {
if e.to_string().contains("not found") {
RegistryError::not_found(e.to_string())
} else {
RegistryError::internal(e.to_string())
}
})?;
Ok((
StatusCode::OK,
Json(json!({
"manifest": response.manifest,
"digest": response.digest,
"content_type": response.content_type,
})),
))
}
/// Push manifest (PUT /v2/<name>/manifests/<reference>)
#[onto_api(
method = "PUT",
path = "/v2/{name}/manifests/{reference}",
description = "Push extension manifest (OCI v2)",
auth = "bearer",
actors = "developer",
tags = "oci, manifests",
feature = ""
)]
async fn push_manifest(
Path((name, reference)): Path<(String, String)>,
State(state): State<AppState>,
Json(manifest): Json<ImageManifest>,
) -> Result<(StatusCode, Json<serde_json::Value>), RegistryError> {
let digest = state
.registry
.put_manifest(format!("{}:{}", name, reference), manifest)
.await
.map_err(|e: anyhow::Error| RegistryError::internal(e.to_string()))?;
Ok((
StatusCode::CREATED,
Json(json!({
"digest": digest,
})),
))
}
/// List repositories catalog (GET /v2/_catalog)
#[onto_api(
method = "GET",
path = "/v2/_catalog",
description = "List all extension repositories (OCI v2)",
auth = "bearer",
actors = "developer, agent",
tags = "oci, catalog",
feature = ""
)]
async fn list_catalog(
State(state): State<AppState>,
) -> Result<Json<serde_json::Value>, RegistryError> {
let extensions = state
.registry
.list_extensions()
.await
.map_err(|e| RegistryError::internal(e.to_string()))?;
let repositories: Vec<String> = extensions.iter().map(|e| e.name.clone()).collect();
Ok(Json(json!({
"repositories": repositories,
})))
}
/// List extension tags (GET /v2/<name>/tags/list)
#[onto_api(
method = "GET",
path = "/v2/{name}/tags/list",
description = "List extension tags (OCI v2)",
auth = "bearer",
actors = "developer, agent",
tags = "oci, tags",
feature = ""
)]
async fn list_tags(
Path(name): Path<String>,
State(state): State<AppState>,
) -> Result<Json<serde_json::Value>, RegistryError> {
let extension = state
.registry
.get_extension(&name)
.await
.map_err(|e: anyhow::Error| {
if e.to_string().contains("not found") {
RegistryError::not_found(e.to_string())
} else {
RegistryError::internal(e.to_string())
}
})?;
Ok(Json(json!({
"name": name,
"tags": [extension.version],
})))
}
/// Register extension metadata (POST /extensions)
#[onto_api(
method = "POST",
path = "/extensions",
description = "Register extension metadata",
auth = "bearer",
actors = "developer",
tags = "extensions",
feature = ""
)]
async fn register_extension(
State(state): State<AppState>,
Json(metadata): Json<ExtensionMetadata>,
) -> Result<(StatusCode, Json<serde_json::Value>), RegistryError> {
state
.registry
.register_extension(metadata.clone())
.await
.map_err(|e: anyhow::Error| RegistryError::internal(e.to_string()))?;
Ok((
StatusCode::CREATED,
Json(json!({
"name": metadata.name,
"version": metadata.version,
})),
))
}
/// Get extension metadata (GET /extensions/:name)
#[onto_api(
method = "GET",
path = "/extensions/{name}",
description = "Get extension metadata by name",
auth = "bearer",
actors = "developer, agent",
tags = "extensions",
feature = ""
)]
async fn get_extension(
Path(name): Path<String>,
State(state): State<AppState>,
) -> Result<Json<ExtensionMetadata>, RegistryError> {
state
.registry
.get_extension(&name)
.await
.map_err(|e: anyhow::Error| {
if e.to_string().contains("not found") {
RegistryError::not_found(e.to_string())
} else {
RegistryError::internal(e.to_string())
}
})
.map(Json)
}
/// List all extensions (GET /extensions)
#[onto_api(
method = "GET",
path = "/extensions",
description = "List all registered extensions",
auth = "bearer",
actors = "developer, agent",
tags = "extensions",
feature = ""
)]
async fn list_extensions(
State(state): State<AppState>,
) -> Result<Json<Vec<ExtensionMetadata>>, RegistryError> {
state
.registry
.list_extensions()
.await
.map_err(|e: anyhow::Error| RegistryError::internal(e.to_string()))
.map(Json)
}
/// Health check (GET /health)
#[onto_api(
method = "GET",
path = "/health",
description = "Service health check",
auth = "none",
actors = "developer, agent, ci",
tags = "health",
feature = ""
)]
async fn health(State(state): State<AppState>) -> Result<StatusCode, RegistryError> {
state
.registry
.health_check()
.await
.map_err(|e: anyhow::Error| RegistryError::internal(e.to_string()))?;
Ok(StatusCode::OK)
}
/// Build API router
pub fn routes(state: AppState) -> Router {
Router::new()
// OCI v2 API
.route("/v2/{name}/blobs/{digest}", head(blob_exists))
.route("/v2/{name}/blobs/{digest}", get(pull_blob))
.route("/v2/{name}/blobs/uploads", post(push_blob))
.route("/v2/{name}/manifests/{reference}", get(pull_manifest))
.route("/v2/{name}/manifests/{reference}", put(push_manifest))
.route("/v2/_catalog", get(list_catalog))
.route("/v2/{name}/tags/list", get(list_tags))
// Extensions API
.route("/extensions", post(register_extension))
.route("/extensions", get(list_extensions))
.route("/extensions/{name}", get(get_extension))
// Health
.route("/health", get(health))
.route("/api/v1/health", get(health))
// API catalog
.route(
"/api/catalog",
get(crate::api_catalog::api_catalog),
)
.with_state(state)
}