1use thiserror::Error;
4
5#[derive(Error, Debug)]
7pub enum OciDistributionError {
8 #[error("Authentication failure: {0}")]
10 AuthenticationFailure(String),
11 #[error("Generic error: {0:?}")]
13 GenericError(Option<String>),
14 #[error(transparent)]
16 HeaderValueError(#[from] reqwest::header::ToStrError),
17 #[error(transparent)]
19 IoError(#[from] std::io::Error),
20 #[error("Received Image Index/Manifest List, but platform_resolver was not defined on the client config. Consider setting platform_resolver")]
22 ImageIndexParsingNoPlatformResolverError,
23 #[error("Image manifest not found: {0}")]
25 ImageManifestNotFoundError(String),
26 #[error("Incompatible layer media type: {0}")]
28 IncompatibleLayerMediaTypeError(String),
29 #[error(transparent)]
30 JsonError(#[from] serde_json::error::Error),
32 #[error("Manifest is not valid UTF-8")]
34 ManifestEncodingError(#[from] std::str::Utf8Error),
35 #[error("Failed to parse manifest as Versioned object: {0}")]
37 ManifestParsingError(String),
38 #[error("cannot push a blob without data")]
40 PushNoDataError,
41 #[error("cannot push a layer without data")]
43 PushLayerNoDataError,
44 #[error("No layers to pull")]
46 PullNoLayersError,
47 #[error("Registry error: url {url}, envelope: {envelope}")]
49 RegistryError {
50 envelope: OciEnvelope,
52 url: String,
54 },
55 #[error("Registry did not return a digest header")]
57 RegistryNoDigestError,
58 #[error("Registry did not return a location header")]
60 RegistryNoLocationError,
61 #[error("Failed to decode registry token: {0}")]
63 RegistryTokenDecodeError(String),
64 #[error(transparent)]
66 RequestError(#[from] reqwest::Error),
67 #[error("Error parsing Url {0}")]
69 UrlParseError(String),
70 #[error("Server error: url {url}, code: {code}, message: {message}")]
72 ServerError {
73 code: u16,
75 url: String,
77 message: String,
79 },
80 #[error("OCI distribution spec violation: {0}")]
83 SpecViolationError(String),
84 #[error("Not authorized: url {url}")]
86 UnauthorizedError {
87 url: String,
89 },
90 #[error("Unsupported media type: {0}")]
92 UnsupportedMediaTypeError(String),
93 #[error("Unsupported schema version: {0}")]
95 UnsupportedSchemaVersionError(i32),
96 #[error("Failed to parse manifest: {0}")]
98 VersionedParsingError(String),
99 #[error("Failed to convert Config into ConfigFile: {0}")]
100 ConfigConversionError(String),
102}
103
104pub type Result<T> = std::result::Result<T, OciDistributionError>;
106
107#[derive(serde::Deserialize, Debug)]
112pub struct OciError {
113 pub code: OciErrorCode,
115 #[serde(default)]
117 pub message: String,
118 #[serde(default)]
120 pub detail: serde_json::Value,
121}
122
123impl std::error::Error for OciError {
124 fn description(&self) -> &str {
125 self.message.as_str()
126 }
127}
128impl std::fmt::Display for OciError {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 write!(f, "OCI API error: {}", self.message.as_str())
131 }
132}
133
134#[derive(serde::Deserialize, Debug)]
136pub struct OciEnvelope {
137 pub errors: Vec<OciError>,
139}
140
141impl std::fmt::Display for OciEnvelope {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 let errors: Vec<String> = self.errors.iter().map(|e| e.to_string()).collect();
144 write!(f, "OCI API errors: [{}]", errors.join("\n"))
145 }
146}
147
148#[derive(serde::Deserialize, Debug, PartialEq, Eq)]
152#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
153pub enum OciErrorCode {
154 BlobUnknown,
160 BlobUploadInvalid,
164 BlobUploadUnknown,
166 DigestInvalid,
168 ManifestBlobUnknown,
170 ManifestInvalid,
177 ManifestUnknown,
181 ManifestUnverified,
185 NameInvalid,
187 NameUnknown,
189 SizeInvalid,
191 TagInvalid,
195 Unauthorized,
197 Denied,
199 Unsupported,
201 Toomanyrequests,
203}
204
205#[cfg(test)]
206mod test {
207 use super::*;
208
209 const EXAMPLE_ERROR: &str = r#"
210 {"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":[{"Type":"repository","Name":"hello-wasm","Action":"pull"}]}]}
211 "#;
212 #[test]
213 fn test_deserialize() {
214 let envelope: OciEnvelope =
215 serde_json::from_str(EXAMPLE_ERROR).expect("parse example error");
216 let e = &envelope.errors[0];
217 assert_eq!(OciErrorCode::Unauthorized, e.code);
218 assert_eq!("authentication required", e.message);
219 assert_ne!(serde_json::value::Value::Null, e.detail);
220 }
221
222 const EXAMPLE_ERROR_TOOMANYREQUESTS: &str = r#"
223 {"errors":[{"code":"TOOMANYREQUESTS","message":"pull request limit exceeded","detail":"You have reached your pull rate limit."}]}
224 "#;
225 #[test]
226 fn test_deserialize_toomanyrequests() {
227 let envelope: OciEnvelope =
228 serde_json::from_str(EXAMPLE_ERROR_TOOMANYREQUESTS).expect("parse example error");
229 let e = &envelope.errors[0];
230 assert_eq!(OciErrorCode::Toomanyrequests, e.code);
231 assert_eq!("pull request limit exceeded", e.message);
232 assert_ne!(serde_json::value::Value::Null, e.detail);
233 }
234
235 const EXAMPLE_ERROR_MISSING_MESSAGE: &str = r#"
236 {"errors":[{"code":"UNAUTHORIZED","detail":[{"Type":"repository","Name":"hello-wasm","Action":"pull"}]}]}
237 "#;
238 #[test]
239 fn test_deserialize_without_message_field() {
240 let envelope: OciEnvelope =
241 serde_json::from_str(EXAMPLE_ERROR_MISSING_MESSAGE).expect("parse example error");
242 let e = &envelope.errors[0];
243 assert_eq!(OciErrorCode::Unauthorized, e.code);
244 assert_eq!(String::default(), e.message);
245 assert_ne!(serde_json::value::Value::Null, e.detail);
246 }
247
248 const EXAMPLE_ERROR_MISSING_DETAIL: &str = r#"
249 {"errors":[{"code":"UNAUTHORIZED","message":"authentication required"}]}
250 "#;
251 #[test]
252 fn test_deserialize_without_detail_field() {
253 let envelope: OciEnvelope =
254 serde_json::from_str(EXAMPLE_ERROR_MISSING_DETAIL).expect("parse example error");
255 let e = &envelope.errors[0];
256 assert_eq!(OciErrorCode::Unauthorized, e.code);
257 assert_eq!("authentication required", e.message);
258 assert_eq!(serde_json::value::Value::Null, e.detail);
259 }
260}