Commit

Author:

Hash:

Timestamp:

+86 -24 +/-5 browse

Kevin Schoon [me@kevinschoon.com]

ea887ad0ec449db678e9131ca115cb8475714132

Sat, 05 Apr 2025 10:23:56 +0000 (4 months ago)

add write manifest implementation
1diff --git a/src/address.rs b/src/address.rs
2index 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
34index 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
51index 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
152index 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
190index 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}",