Author:
Hash:
Timestamp:
+86 -24 +/-5 browse
Kevin Schoon [me@kevinschoon.com]
ea887ad0ec449db678e9131ca115cb8475714132
Sat, 05 Apr 2025 10:23:56 +0000 (6 months ago)
| 1 | diff --git a/src/address.rs b/src/address.rs |
| 2 | index d8032c9..ea16ed4 100644 |
| 3 | --- a/src/address.rs |
| 4 | +++ b/src/address.rs |
| 5 | @@ -11,7 +11,7 @@ use crate::Namespace; |
| 6 | const SEPARATOR: &str = "/"; |
| 7 | |
| 8 | /// Address is a path-like object for addressing OCI objects. The design of |
| 9 | - /// this basically copies the file system layout used by the Docker |
| 10 | + /// this basically copies the file system layout used by the Docker |
| 11 | /// distribution registry. https://github.com/distribution/distribution |
| 12 | #[derive(Debug, Clone)] |
| 13 | pub struct Address { |
| 14 | @@ -131,6 +131,18 @@ impl From<Blob<'_>> for Address { |
| 15 | } |
| 16 | } |
| 17 | |
| 18 | + /// Manifest is a link to a blob on disk |
| 19 | + pub struct Manifest<'a> { |
| 20 | + pub namespace: &'a Namespace, |
| 21 | + pub digest: &'a Digest, |
| 22 | + } |
| 23 | + |
| 24 | + impl From<Manifest<'_>> for Address { |
| 25 | + fn from(value: Manifest<'_>) -> Self { |
| 26 | + todo!() |
| 27 | + } |
| 28 | + } |
| 29 | + |
| 30 | #[cfg(test)] |
| 31 | mod test { |
| 32 | use std::str::FromStr; |
| 33 | diff --git a/src/error.rs b/src/error.rs |
| 34 | index 724dd18..495f30b 100644 |
| 35 | --- a/src/error.rs |
| 36 | +++ b/src/error.rs |
| 37 | @@ -85,7 +85,11 @@ impl From<&Error> for Message { |
| 38 | message: String::from("Storage level failure"), |
| 39 | detail: Some(error.to_string()), |
| 40 | }, |
| 41 | - Error::OciInternal(oci_spec_error) => todo!(), |
| 42 | + Error::OciInternal(oci_spec_error) => Message { |
| 43 | + code: String::from("OCI_SPEC_ERROR"), |
| 44 | + message: String::from("Failed to parse OCI specification"), |
| 45 | + detail: Some(oci_spec_error.to_string()), |
| 46 | + }, |
| 47 | Error::OciParsing(parse_error) => todo!(), |
| 48 | Error::Stream(_) => todo!(), |
| 49 | } |
| 50 | diff --git a/src/handlers.rs b/src/handlers.rs |
| 51 | index 617f847..1a1648e 100644 |
| 52 | --- a/src/handlers.rs |
| 53 | +++ b/src/handlers.rs |
| 54 | @@ -2,10 +2,12 @@ use std::str::FromStr; |
| 55 | |
| 56 | use axum::{ |
| 57 | Extension, Json, |
| 58 | - extract::{Path, Query, Request, State}, |
| 59 | + body::HttpBody, |
| 60 | + extract::{FromRequest, Path, Query, Request, State}, |
| 61 | http::StatusCode, |
| 62 | - response::Response, |
| 63 | + response::{IntoResponse, Response}, |
| 64 | }; |
| 65 | + use bytes::{Buf, Bytes}; |
| 66 | use futures::TryStreamExt; |
| 67 | use http::header::CONTENT_TYPE; |
| 68 | use oci_spec::{ |
| 69 | @@ -18,8 +20,6 @@ use uuid::Uuid; |
| 70 | |
| 71 | use crate::{Namespace, error::Error, routes::AppState}; |
| 72 | |
| 73 | - // pub type Result<T = Json<serde_json::Value>, E = Error> = std::result::Result<T, E>; |
| 74 | - |
| 75 | pub async fn index() -> Result<Json<serde_json::Value>, Error> { |
| 76 | Ok(Json(json!({}))) |
| 77 | } |
| 78 | @@ -44,9 +44,8 @@ pub async fn read_blob( |
| 79 | |
| 80 | pub async fn stat_blob( |
| 81 | State(state): State<AppState>, |
| 82 | - Path((name, digest)): Path<(String, String)>, |
| 83 | + Path(digest): Path<String>, |
| 84 | ) -> Result<StatusCode, Error> { |
| 85 | - let _ = Namespace::from_str(&name)?; |
| 86 | let digest = Digest::from_str(&digest)?; |
| 87 | if !state.oci.has_blob(&digest).await? { |
| 88 | Ok(StatusCode::NOT_FOUND) |
| 89 | @@ -55,24 +54,50 @@ pub async fn stat_blob( |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | + /// Extracts the manifest but also retains the exact byte specification of the |
| 94 | + /// client manifest input. |
| 95 | + pub struct ManifestExtractor((Bytes, ImageManifest)); |
| 96 | + |
| 97 | + impl<S> FromRequest<S> for ManifestExtractor |
| 98 | + where |
| 99 | + S: Send + Sync, |
| 100 | + { |
| 101 | + type Rejection = Error; |
| 102 | + |
| 103 | + async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> { |
| 104 | + let headers = req.headers(); |
| 105 | + let Some(content_type) = headers.get(CONTENT_TYPE) else { |
| 106 | + todo!() |
| 107 | + }; |
| 108 | + let content_type_str = content_type.to_str().unwrap(); |
| 109 | + let media_type = MediaType::from(content_type_str); |
| 110 | + if !matches!(media_type, MediaType::ImageManifest) { |
| 111 | + todo!() |
| 112 | + } |
| 113 | + let body = Bytes::from_request(req, state) |
| 114 | + .await |
| 115 | + .map_err(|err| Error::Stream(err.to_string()))?; |
| 116 | + |
| 117 | + let buf = body.as_ref().reader(); |
| 118 | + let manifest = ImageManifest::from_reader(buf)?; |
| 119 | + Ok(ManifestExtractor((body, manifest))) |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + /// NOTE: The registry MUST store the manifest in the exact byte representation |
| 124 | + /// provided by the client. |
| 125 | #[axum::debug_handler] |
| 126 | pub async fn write_manifest( |
| 127 | Extension(namespace): Extension<Namespace>, |
| 128 | State(state): State<AppState>, |
| 129 | Path(digest): Path<String>, |
| 130 | - headers: http::header::HeaderMap, |
| 131 | - axum::extract::Json(manifest): axum::extract::Json<ImageManifest>, |
| 132 | + ManifestExtractor((manifest_bytes, manifest)): ManifestExtractor, |
| 133 | ) -> Result<StatusCode, Error> { |
| 134 | let digest = Digest::from_str(&digest)?; |
| 135 | - let Some(content_type) = headers.get(CONTENT_TYPE) else { |
| 136 | - todo!() |
| 137 | - }; |
| 138 | - let content_type_str = content_type.to_str().unwrap(); |
| 139 | - let media_type = MediaType::from(content_type_str); |
| 140 | - if !matches!(media_type, MediaType::ImageManifest) { |
| 141 | - todo!() |
| 142 | - } |
| 143 | - state.oci.write_manifest(&namespace, &manifest).await?; |
| 144 | + state |
| 145 | + .oci |
| 146 | + .write_manifest(&namespace, &digest, &manifest, &manifest_bytes) |
| 147 | + .await?; |
| 148 | Ok(StatusCode::OK) |
| 149 | } |
| 150 | |
| 151 | diff --git a/src/oci_interface.rs b/src/oci_interface.rs |
| 152 | index aca106d..769095a 100644 |
| 153 | --- a/src/oci_interface.rs |
| 154 | +++ b/src/oci_interface.rs |
| 155 | @@ -11,7 +11,7 @@ use uuid::Uuid; |
| 156 | |
| 157 | use crate::{ |
| 158 | Namespace, |
| 159 | - address::{Address, Blob, TempBlob}, |
| 160 | + address::{Address, Blob, Manifest, TempBlob}, |
| 161 | error::Error, |
| 162 | storage::Storage, |
| 163 | }; |
| 164 | @@ -67,9 +67,23 @@ impl OciInterface { |
| 165 | pub async fn write_manifest( |
| 166 | &self, |
| 167 | namespace: &Namespace, |
| 168 | + digest: &Digest, |
| 169 | manifest: &ImageManifest, |
| 170 | + manifest_bytes: &Bytes, |
| 171 | ) -> Result<(), Error> { |
| 172 | - todo!() |
| 173 | + let blob: Address = Blob::from(digest).into(); |
| 174 | + self.storage |
| 175 | + .write_all(&blob.data(), manifest_bytes.to_vec().as_slice()) |
| 176 | + .await |
| 177 | + .map_err(Error::Storage)?; |
| 178 | + self.storage |
| 179 | + .write_all( |
| 180 | + &crate::address::Manifest { namespace, digest }.into(), |
| 181 | + digest.to_string().as_bytes(), |
| 182 | + ) |
| 183 | + .await |
| 184 | + .map_err(Error::Storage)?; |
| 185 | + Ok(()) |
| 186 | } |
| 187 | |
| 188 | pub async fn read_manifest( |
| 189 | diff --git a/src/routes.rs b/src/routes.rs |
| 190 | index d59ae6d..fd1e36c 100644 |
| 191 | --- a/src/routes.rs |
| 192 | +++ b/src/routes.rs |
| 193 | @@ -2,8 +2,8 @@ use std::io::Bytes; |
| 194 | use std::str::FromStr; |
| 195 | use std::sync::Arc; |
| 196 | |
| 197 | - use axum::extract::Request; |
| 198 | - use axum::routing::{post, put}; |
| 199 | + use axum::extract::{DefaultBodyLimit, Request}; |
| 200 | + use axum::routing::{head, post, put}; |
| 201 | use axum::{Router, routing::get}; |
| 202 | use http::Uri; |
| 203 | |
| 204 | @@ -55,12 +55,19 @@ pub fn extract_namespace(mut req: Request<axum::body::Body>) -> Request<axum::bo |
| 205 | req |
| 206 | } |
| 207 | |
| 208 | + const MAXIMUM_MANIFEST_SIZE: usize = 5_000_000; |
| 209 | + |
| 210 | pub fn router(storage: impl Storage + 'static) -> Router { |
| 211 | Router::new() |
| 212 | .route("/v2", get(crate::handlers::index)) |
| 213 | .route("/upload/{uuid}", put(crate::handlers::write_blob)) |
| 214 | .route("/blobs/uploads", post(crate::handlers::initiate_blob)) |
| 215 | - .route("/manifests/{digest}", put(crate::handlers::write_manifest)) |
| 216 | + .route("/blobs/{digest}", head(crate::handlers::stat_blob)) |
| 217 | + .route( |
| 218 | + "/manifests/{digest}", |
| 219 | + put(crate::handlers::write_manifest) |
| 220 | + .layer(DefaultBodyLimit::max(MAXIMUM_MANIFEST_SIZE)), |
| 221 | + ) |
| 222 | // // .route("/{name}/blobs/{digest}", head(crate::handlers::stat_blob)) |
| 223 | // // .route( |
| 224 | // // "/{name}/manifests/{reference}", |