Author:
Hash:
Timestamp:
+86 -24 +/-5 browse
Kevin Schoon [me@kevinschoon.com]
ea887ad0ec449db678e9131ca115cb8475714132
Sat, 05 Apr 2025 10:23:56 +0000 (4 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}", |