Rust implementation of OCI Distribution Spec with granular access control
1use std::sync::Arc;
2
3use axum::{
4 extract::DefaultBodyLimit,
5 routing::{delete, get, head, patch, post, put},
6 Router,
7};
8use clap::Parser;
9use tower_http::cors::CorsLayer;
10use utoipa::OpenApi;
11use utoipa_swagger_ui::SwaggerUi;
12
13mod admin;
14mod args;
15mod auth;
16mod blobs;
17mod errors;
18mod gc;
19mod health;
20mod manifests;
21mod meta;
22mod metrics;
23mod middleware;
24mod openapi;
25mod permissions;
26mod response;
27mod state;
28mod storage;
29mod tags;
30mod utils;
31mod validation;
32
33#[tokio::main]
34async fn main() {
35 let args = args::Args::parse();
36 env_logger::init();
37 log::info!("Starting grain build: {}", utils::get_build_info());
38
39 // Shared app state
40 let shared_state = Arc::new(state::new_app(&args));
41 let state_clone = shared_state.clone();
42
43 let app = Router::new()
44 .route("/", get(meta::index)) // Index, info
45 // Health endpoints (no auth required)
46 .route("/health", get(health::health))
47 .route("/health/live", get(health::liveness))
48 .route("/health/ready", get(health::readiness))
49 // Metrics endpoint (no auth for Prometheus scraping)
50 .route("/metrics", get(metrics::metrics))
51 .route("/v2/", get(auth::get)) // end-1
52 .route(
53 "/v2/{org}/{repo}/manifests/{reference}",
54 head(manifests::head_manifest_by_reference),
55 )
56 .route(
57 "/v2/{org}/{repo}/manifests/{reference}",
58 get(manifests::get_manifest_by_reference),
59 )
60 .route(
61 "/v2/{org}/{repo}/blobs/{digest}",
62 get(blobs::get_blob_by_digest),
63 ) // end-2
64 .route(
65 "/v2/{org}/{repo}/blobs/{digest}",
66 head(blobs::head_blob_by_digest),
67 )
68 .route(
69 "/v2/{org}/{repo}/blobs/uploads/",
70 post(blobs::post_blob_upload),
71 ) // end-4a, end-4b, end-11
72 .route(
73 "/v2/{org}/{repo}/blobs/uploads/{reference}",
74 patch(blobs::patch_blob_upload),
75 ) // end-5
76 .route(
77 "/v2/{org}/{repo}/blobs/uploads/{reference}",
78 put(blobs::put_blob_upload_by_reference),
79 ) // end-6
80 .route(
81 "/v2/{org}/{repo}/manifests/{reference}",
82 put(manifests::put_manifest_by_reference),
83 ) // end-7
84 .route("/v2/{org}/{repo}/tags/list", get(tags::get_tags_list)) // end-8a, end-8b
85 .route(
86 "/v2/{org}/{repo}/manifests/{reference}",
87 delete(manifests::delete_manifest_by_reference),
88 ) // end-9
89 .route(
90 "/v2/{org}/{repo}/blobs/{digest}",
91 delete(blobs::delete_blob_by_digest),
92 ) // end-10
93 // Admin API routes
94 .route("/admin/users", get(admin::list_users))
95 .route("/admin/users", post(admin::create_user))
96 .route("/admin/users/{username}", delete(admin::delete_user))
97 .route(
98 "/admin/users/{username}/permissions",
99 post(admin::add_permission),
100 )
101 .route(
102 "/admin/permissions",
103 post(admin::add_permission_with_username),
104 )
105 .route("/admin/gc", post(admin::run_garbage_collection))
106 // Catch-all routes for debugging
107 .route("/{*path}", head(meta::catch_all_head))
108 .route("/{*path}", get(meta::catch_all_get))
109 .route("/{*path}", post(meta::catch_all_post))
110 .route("/{*path}", put(meta::catch_all_put))
111 .route("/{*path}", patch(meta::catch_all_patch))
112 .route("/{*path}", delete(meta::catch_all_delete))
113 .with_state(state_clone)
114 .layer(DefaultBodyLimit::disable()) // Allow unlimited body size for blob uploads
115 .layer(axum::middleware::from_fn(middleware::track_metrics))
116 .layer(CorsLayer::permissive())
117 .merge(
118 SwaggerUi::new("/swagger-ui")
119 .url("/api-docs/openapi.json", openapi::AdminApiDoc::openapi()),
120 );
121
122 log::info!("Listening on: {}", &args.host);
123 let listener = tokio::net::TcpListener::bind(&args.host).await.unwrap();
124
125 // Mark server as ready after successful bind
126 {
127 let mut status = shared_state.server_status.lock().await;
128 *status = state::ServerStatus::Ready;
129 log::info!("Server status: Ready");
130 }
131
132 axum::serve(listener, app).await.unwrap();
133}