oci_distribution/
errors.rs

1//! Errors related to interacting with an OCI compliant remote store
2
3use thiserror::Error;
4
5/// Errors that can be raised while interacting with an OCI registry
6#[derive(Error, Debug)]
7pub enum OciDistributionError {
8    /// Authentication error
9    #[error("Authentication failure: {0}")]
10    AuthenticationFailure(String),
11    /// Generic error, might provide an explanation message
12    #[error("Generic error: {0:?}")]
13    GenericError(Option<String>),
14    /// Transparent wrapper around `reqwest::header::ToStrError`
15    #[error(transparent)]
16    HeaderValueError(#[from] reqwest::header::ToStrError),
17    /// IO Error
18    #[error(transparent)]
19    IoError(#[from] std::io::Error),
20    /// Platform resolver not specified
21    #[error("Received Image Index/Manifest List, but platform_resolver was not defined on the client config. Consider setting platform_resolver")]
22    ImageIndexParsingNoPlatformResolverError,
23    /// Image manifest not found
24    #[error("Image manifest not found: {0}")]
25    ImageManifestNotFoundError(String),
26    /// Registry returned a layer with an incompatible type
27    #[error("Incompatible layer media type: {0}")]
28    IncompatibleLayerMediaTypeError(String),
29    #[error(transparent)]
30    /// Transparent wrapper around `serde_json::error::Error`
31    JsonError(#[from] serde_json::error::Error),
32    /// Manifest is not valid UTF-8
33    #[error("Manifest is not valid UTF-8")]
34    ManifestEncodingError(#[from] std::str::Utf8Error),
35    /// Manifest: JSON unmarshalling error
36    #[error("Failed to parse manifest as Versioned object: {0}")]
37    ManifestParsingError(String),
38    /// Cannot push a blob without data
39    #[error("cannot push a blob without data")]
40    PushNoDataError,
41    /// Cannot push layer object without data
42    #[error("cannot push a layer without data")]
43    PushLayerNoDataError,
44    /// No layers available to be pulled
45    #[error("No layers to pull")]
46    PullNoLayersError,
47    /// OCI registry error
48    #[error("Registry error: url {url}, envelope: {envelope}")]
49    RegistryError {
50        /// List of errors returned the by the OCI registry
51        envelope: OciEnvelope,
52        /// Request URL
53        url: String,
54    },
55    /// Registry didn't return a Digest object
56    #[error("Registry did not return a digest header")]
57    RegistryNoDigestError,
58    /// Registry didn't return a Location header
59    #[error("Registry did not return a location header")]
60    RegistryNoLocationError,
61    /// Registry token: JSON deserialization error
62    #[error("Failed to decode registry token: {0}")]
63    RegistryTokenDecodeError(String),
64    /// Transparent wrapper around `reqwest::Error`
65    #[error(transparent)]
66    RequestError(#[from] reqwest::Error),
67    /// Cannot parse URL
68    #[error("Error parsing Url {0}")]
69    UrlParseError(String),
70    /// HTTP Server error
71    #[error("Server error: url {url}, code: {code}, message: {message}")]
72    ServerError {
73        /// HTTP status code
74        code: u16,
75        /// Request URL
76        url: String,
77        /// Error message returned by the remote server
78        message: String,
79    },
80    /// The [OCI distribution spec](https://github.com/opencontainers/distribution-spec/blob/main/spec.md)
81    /// is not respected by the remote registry
82    #[error("OCI distribution spec violation: {0}")]
83    SpecViolationError(String),
84    /// HTTP auth failed - user not authorized
85    #[error("Not authorized: url {url}")]
86    UnauthorizedError {
87        /// request URL
88        url: String,
89    },
90    /// Media type not supported
91    #[error("Unsupported media type: {0}")]
92    UnsupportedMediaTypeError(String),
93    /// Schema version not supported
94    #[error("Unsupported schema version: {0}")]
95    UnsupportedSchemaVersionError(i32),
96    /// Versioned object: JSON deserialization error
97    #[error("Failed to parse manifest: {0}")]
98    VersionedParsingError(String),
99    #[error("Failed to convert Config into ConfigFile: {0}")]
100    /// Transparent wrapper around `std::string::FromUtf8Error`
101    ConfigConversionError(String),
102}
103
104/// Helper type to declare `Result` objects that might return a `OciDistributionError`
105pub type Result<T> = std::result::Result<T, OciDistributionError>;
106
107/// The OCI specification defines a specific error format.
108///
109/// This struct represents that error format, which is formally described here:
110/// <https://github.com/opencontainers/distribution-spec/blob/master/spec.md#errors-2>
111#[derive(serde::Deserialize, Debug)]
112pub struct OciError {
113    /// The error code
114    pub code: OciErrorCode,
115    /// An optional message associated with the error
116    #[serde(default)]
117    pub message: String,
118    /// Unstructured optional data associated with the error
119    #[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/// A struct that holds a series of OCI errors
135#[derive(serde::Deserialize, Debug)]
136pub struct OciEnvelope {
137    /// List of OCI registry errors
138    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/// OCI error codes
149///
150/// Outlined [here](https://github.com/opencontainers/distribution-spec/blob/master/spec.md#errors-2)
151#[derive(serde::Deserialize, Debug, PartialEq, Eq)]
152#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
153pub enum OciErrorCode {
154    /// Blob unknown to registry
155    ///
156    /// This error MAY be returned when a blob is unknown to the registry in a specified
157    /// repository. This can be returned with a standard get or if a manifest
158    /// references an unknown layer during upload.
159    BlobUnknown,
160    /// Blob upload is invalid
161    ///
162    /// The blob upload encountered an error and can no longer proceed.
163    BlobUploadInvalid,
164    /// Blob upload is unknown to registry
165    BlobUploadUnknown,
166    /// Provided digest did not match uploaded content.
167    DigestInvalid,
168    /// Blob is unknown to registry
169    ManifestBlobUnknown,
170    /// Manifest is invalid
171    ///
172    /// During upload, manifests undergo several checks ensuring validity. If
173    /// those checks fail, this error MAY be returned, unless a more specific
174    /// error is included. The detail will contain information the failed
175    /// validation.
176    ManifestInvalid,
177    /// Manifest unknown
178    ///
179    /// This error is returned when the manifest, identified by name and tag is unknown to the repository.
180    ManifestUnknown,
181    /// Manifest failed signature validation
182    ///
183    /// DEPRECATED: This error code has been removed from the OCI spec.
184    ManifestUnverified,
185    /// Invalid repository name
186    NameInvalid,
187    /// Repository name is not known
188    NameUnknown,
189    /// Provided length did not match content length
190    SizeInvalid,
191    /// Manifest tag did not match URI
192    ///
193    /// DEPRECATED: This error code has been removed from the OCI spec.
194    TagInvalid,
195    /// Authentication required.
196    Unauthorized,
197    /// Requested access to the resource is denied
198    Denied,
199    /// This operation is unsupported
200    Unsupported,
201    /// Too many requests from client
202    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}