oci_client/
errors.rs

1//! Errors related to interacting with an OCI compliant remote store
2
3use thiserror::Error;
4
5pub use crate::digest::DigestError;
6
7/// Errors that can be raised while interacting with an OCI registry
8#[derive(Error, Debug)]
9pub enum OciDistributionError {
10    /// Authentication error
11    #[error("Authentication failure: {0}")]
12    AuthenticationFailure(String),
13    #[error("Failed to convert Config into ConfigFile: {0}")]
14    /// Transparent wrapper around `std::string::FromUtf8Error`
15    ConfigConversionError(String),
16    /// An error occurred with a digest operation
17    #[error("Digest error: {0}")]
18    DigestError(#[from] DigestError),
19    /// Generic error, might provide an explanation message
20    #[error("Generic error: {0:?}")]
21    GenericError(Option<String>),
22    /// Transparent wrapper around `reqwest::header::ToStrError`
23    #[error(transparent)]
24    HeaderValueError(#[from] reqwest::header::ToStrError),
25    /// Image manifest not found
26    #[error("Image manifest not found: {0}")]
27    ImageManifestNotFoundError(String),
28    /// Platform resolver not specified
29    #[error("Received Image Index/Manifest List, but platform_resolver was not defined on the client config. Consider setting platform_resolver")]
30    ImageIndexParsingNoPlatformResolverError,
31    /// Registry returned a layer with an incompatible type
32    #[error("Incompatible layer media type: {0}")]
33    IncompatibleLayerMediaTypeError(String),
34    /// IO Error
35    #[error(transparent)]
36    IoError(#[from] std::io::Error),
37    #[error(transparent)]
38    /// Transparent wrapper around `serde_json::error::Error`
39    JsonError(#[from] serde_json::error::Error),
40    /// Manifest is not valid UTF-8
41    #[error("Manifest is not valid UTF-8")]
42    ManifestEncodingError(#[from] std::str::Utf8Error),
43    /// Manifest: JSON unmarshalling error
44    #[error("Failed to parse manifest as Versioned object: {0}")]
45    ManifestParsingError(String),
46    /// Cannot push a blob without data
47    #[error("cannot push a blob without data")]
48    PushNoDataError,
49    /// Cannot push layer object without data
50    #[error("cannot push a layer without data")]
51    PushLayerNoDataError,
52    /// No layers available to be pulled
53    #[error("No layers to pull")]
54    PullNoLayersError,
55    /// OCI registry error
56    #[error("Registry error: url {url}, envelope: {envelope}")]
57    RegistryError {
58        /// List of errors returned the by the OCI registry
59        envelope: OciEnvelope,
60        /// Request URL
61        url: String,
62    },
63    /// Registry didn't return a Digest object
64    #[error("Registry did not return a digest header")]
65    RegistryNoDigestError,
66    /// Registry didn't return a Location header
67    #[error("Registry did not return a location header")]
68    RegistryNoLocationError,
69    /// Registry token: JSON deserialization error
70    #[error("Failed to decode registry token: {0}")]
71    RegistryTokenDecodeError(String),
72    /// Transparent wrapper around `reqwest::Error`
73    #[error(transparent)]
74    RequestError(#[from] reqwest::Error),
75    /// HTTP Server error
76    #[error("Server error: url {url}, code: {code}, message: {message}")]
77    ServerError {
78        /// HTTP status code
79        code: u16,
80        /// Request URL
81        url: String,
82        /// Error message returned by the remote server
83        message: String,
84    },
85    /// The [OCI distribution spec](https://github.com/opencontainers/distribution-spec/blob/main/spec.md)
86    /// is not respected by the remote registry
87    #[error("OCI distribution spec violation: {0}")]
88    SpecViolationError(String),
89    /// HTTP auth failed - user not authorized
90    #[error("Not authorized: url {url}")]
91    UnauthorizedError {
92        /// request URL
93        url: String,
94    },
95    /// Cannot parse URL
96    #[error("Error parsing Url {0}")]
97    UrlParseError(String),
98    /// Media type not supported
99    #[error("Unsupported media type: {0}")]
100    UnsupportedMediaTypeError(String),
101    /// Schema version not supported
102    #[error("Unsupported schema version: {0}")]
103    UnsupportedSchemaVersionError(i32),
104    /// Versioned object: JSON deserialization error
105    #[error("Failed to parse manifest: {0}")]
106    VersionedParsingError(String),
107}
108
109/// Helper type to declare `Result` objects that might return a `OciDistributionError`
110pub type Result<T> = std::result::Result<T, OciDistributionError>;
111
112/// The OCI specification defines a specific error format.
113///
114/// This struct represents that error format, which is formally described here:
115/// <https://github.com/opencontainers/distribution-spec/blob/master/spec.md#errors-2>
116#[derive(serde::Deserialize, serde::Serialize, Debug)]
117pub struct OciError {
118    /// The error code
119    pub code: OciErrorCode,
120    /// An optional message associated with the error
121    #[serde(default)]
122    pub message: String,
123    /// Unstructured optional data associated with the error
124    #[serde(default)]
125    pub detail: serde_json::Value,
126}
127
128impl std::error::Error for OciError {
129    fn description(&self) -> &str {
130        self.message.as_str()
131    }
132}
133impl std::fmt::Display for OciError {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        write!(f, "OCI API error: {}", self.message.as_str())
136    }
137}
138
139/// A struct that holds a series of OCI errors
140#[derive(serde::Deserialize, serde::Serialize, Debug)]
141pub struct OciEnvelope {
142    /// List of OCI registry errors
143    pub errors: Vec<OciError>,
144}
145
146impl std::fmt::Display for OciEnvelope {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        let errors: Vec<String> = self.errors.iter().map(|e| e.to_string()).collect();
149        write!(f, "OCI API errors: [{}]", errors.join("\n"))
150    }
151}
152
153/// OCI error codes
154///
155/// Outlined [here](https://github.com/opencontainers/distribution-spec/blob/master/spec.md#errors-2)
156#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq)]
157#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
158pub enum OciErrorCode {
159    /// Blob unknown to registry
160    ///
161    /// This error MAY be returned when a blob is unknown to the registry in a specified
162    /// repository. This can be returned with a standard get or if a manifest
163    /// references an unknown layer during upload.
164    BlobUnknown,
165    /// Blob upload is invalid
166    ///
167    /// The blob upload encountered an error and can no longer proceed.
168    BlobUploadInvalid,
169    /// Blob upload is unknown to registry
170    BlobUploadUnknown,
171    /// Provided digest did not match uploaded content.
172    DigestInvalid,
173    /// Blob is unknown to registry
174    ManifestBlobUnknown,
175    /// Manifest is invalid
176    ///
177    /// During upload, manifests undergo several checks ensuring validity. If
178    /// those checks fail, this error MAY be returned, unless a more specific
179    /// error is included. The detail will contain information the failed
180    /// validation.
181    ManifestInvalid,
182    /// Manifest unknown
183    ///
184    /// This error is returned when the manifest, identified by name and tag is unknown to the repository.
185    ManifestUnknown,
186    /// Manifest failed signature validation
187    ///
188    /// DEPRECATED: This error code has been removed from the OCI spec.
189    ManifestUnverified,
190    /// Invalid repository name
191    NameInvalid,
192    /// Repository name is not known
193    NameUnknown,
194    /// Manifest is not found
195    NotFound,
196    /// Provided length did not match content length
197    SizeInvalid,
198    /// Manifest tag did not match URI
199    ///
200    /// DEPRECATED: This error code has been removed from the OCI spec.
201    TagInvalid,
202    /// Authentication required.
203    Unauthorized,
204    /// Requested access to the resource is denied
205    Denied,
206    /// This operation is unsupported
207    Unsupported,
208    /// Too many requests from client
209    Toomanyrequests,
210}
211
212#[cfg(test)]
213mod test {
214    use super::*;
215
216    const EXAMPLE_ERROR: &str = r#"
217      {"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":[{"Type":"repository","Name":"hello-wasm","Action":"pull"}]}]}
218      "#;
219    #[test]
220    fn test_deserialize() {
221        let envelope: OciEnvelope =
222            serde_json::from_str(EXAMPLE_ERROR).expect("parse example error");
223        let e = &envelope.errors[0];
224        assert_eq!(OciErrorCode::Unauthorized, e.code);
225        assert_eq!("authentication required", e.message);
226        assert_ne!(serde_json::value::Value::Null, e.detail);
227    }
228
229    const EXAMPLE_ERROR_TOOMANYREQUESTS: &str = r#"
230      {"errors":[{"code":"TOOMANYREQUESTS","message":"pull request limit exceeded","detail":"You have reached your pull rate limit."}]}
231      "#;
232    #[test]
233    fn test_deserialize_toomanyrequests() {
234        let envelope: OciEnvelope =
235            serde_json::from_str(EXAMPLE_ERROR_TOOMANYREQUESTS).expect("parse example error");
236        let e = &envelope.errors[0];
237        assert_eq!(OciErrorCode::Toomanyrequests, e.code);
238        assert_eq!("pull request limit exceeded", e.message);
239        assert_ne!(serde_json::value::Value::Null, e.detail);
240    }
241
242    const EXAMPLE_ERROR_MISSING_MESSAGE: &str = r#"
243      {"errors":[{"code":"UNAUTHORIZED","detail":[{"Type":"repository","Name":"hello-wasm","Action":"pull"}]}]}
244      "#;
245    #[test]
246    fn test_deserialize_without_message_field() {
247        let envelope: OciEnvelope =
248            serde_json::from_str(EXAMPLE_ERROR_MISSING_MESSAGE).expect("parse example error");
249        let e = &envelope.errors[0];
250        assert_eq!(OciErrorCode::Unauthorized, e.code);
251        assert_eq!(String::default(), e.message);
252        assert_ne!(serde_json::value::Value::Null, e.detail);
253    }
254
255    const EXAMPLE_ERROR_MISSING_DETAIL: &str = r#"
256      {"errors":[{"code":"UNAUTHORIZED","message":"authentication required"}]}
257      "#;
258    #[test]
259    fn test_deserialize_without_detail_field() {
260        let envelope: OciEnvelope =
261            serde_json::from_str(EXAMPLE_ERROR_MISSING_DETAIL).expect("parse example error");
262        let e = &envelope.errors[0];
263        assert_eq!(OciErrorCode::Unauthorized, e.code);
264        assert_eq!("authentication required", e.message);
265        assert_eq!(serde_json::value::Value::Null, e.detail);
266    }
267}