use thiserror::Error;
pub use crate::digest::DigestError;
#[derive(Error, Debug)]
pub enum OciDistributionError {
#[error("Authentication failure: {0}")]
AuthenticationFailure(String),
#[error("Failed to convert Config into ConfigFile: {0}")]
ConfigConversionError(String),
#[error("Digest error: {0}")]
DigestError(#[from] DigestError),
#[error("Generic error: {0:?}")]
GenericError(Option<String>),
#[error(transparent)]
HeaderValueError(#[from] reqwest::header::ToStrError),
#[error("Image manifest not found: {0}")]
ImageManifestNotFoundError(String),
#[error("Received Image Index/Manifest List, but platform_resolver was not defined on the client config. Consider setting platform_resolver")]
ImageIndexParsingNoPlatformResolverError,
#[error("Incompatible layer media type: {0}")]
IncompatibleLayerMediaTypeError(String),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
JsonError(#[from] serde_json::error::Error),
#[error("Manifest is not valid UTF-8")]
ManifestEncodingError(#[from] std::str::Utf8Error),
#[error("Failed to parse manifest as Versioned object: {0}")]
ManifestParsingError(String),
#[error("cannot push a blob without data")]
PushNoDataError,
#[error("cannot push a layer without data")]
PushLayerNoDataError,
#[error("No layers to pull")]
PullNoLayersError,
#[error("Registry error: url {url}, envelope: {envelope}")]
RegistryError {
envelope: OciEnvelope,
url: String,
},
#[error("Registry did not return a digest header")]
RegistryNoDigestError,
#[error("Registry did not return a location header")]
RegistryNoLocationError,
#[error("Failed to decode registry token: {0}")]
RegistryTokenDecodeError(String),
#[error(transparent)]
RequestError(#[from] reqwest::Error),
#[error("Server error: url {url}, code: {code}, message: {message}")]
ServerError {
code: u16,
url: String,
message: String,
},
#[error("OCI distribution spec violation: {0}")]
SpecViolationError(String),
#[error("Not authorized: url {url}")]
UnauthorizedError {
url: String,
},
#[error("Error parsing Url {0}")]
UrlParseError(String),
#[error("Unsupported media type: {0}")]
UnsupportedMediaTypeError(String),
#[error("Unsupported schema version: {0}")]
UnsupportedSchemaVersionError(i32),
#[error("Failed to parse manifest: {0}")]
VersionedParsingError(String),
}
pub type Result<T> = std::result::Result<T, OciDistributionError>;
#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct OciError {
pub code: OciErrorCode,
#[serde(default)]
pub message: String,
#[serde(default)]
pub detail: serde_json::Value,
}
impl std::error::Error for OciError {
fn description(&self) -> &str {
self.message.as_str()
}
}
impl std::fmt::Display for OciError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "OCI API error: {}", self.message.as_str())
}
}
#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct OciEnvelope {
pub errors: Vec<OciError>,
}
impl std::fmt::Display for OciEnvelope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let errors: Vec<String> = self.errors.iter().map(|e| e.to_string()).collect();
write!(f, "OCI API errors: [{}]", errors.join("\n"))
}
}
#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum OciErrorCode {
BlobUnknown,
BlobUploadInvalid,
BlobUploadUnknown,
DigestInvalid,
ManifestBlobUnknown,
ManifestInvalid,
ManifestUnknown,
ManifestUnverified,
NameInvalid,
NameUnknown,
SizeInvalid,
TagInvalid,
Unauthorized,
Denied,
Unsupported,
Toomanyrequests,
}
#[cfg(test)]
mod test {
use super::*;
const EXAMPLE_ERROR: &str = r#"
{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":[{"Type":"repository","Name":"hello-wasm","Action":"pull"}]}]}
"#;
#[test]
fn test_deserialize() {
let envelope: OciEnvelope =
serde_json::from_str(EXAMPLE_ERROR).expect("parse example error");
let e = &envelope.errors[0];
assert_eq!(OciErrorCode::Unauthorized, e.code);
assert_eq!("authentication required", e.message);
assert_ne!(serde_json::value::Value::Null, e.detail);
}
const EXAMPLE_ERROR_TOOMANYREQUESTS: &str = r#"
{"errors":[{"code":"TOOMANYREQUESTS","message":"pull request limit exceeded","detail":"You have reached your pull rate limit."}]}
"#;
#[test]
fn test_deserialize_toomanyrequests() {
let envelope: OciEnvelope =
serde_json::from_str(EXAMPLE_ERROR_TOOMANYREQUESTS).expect("parse example error");
let e = &envelope.errors[0];
assert_eq!(OciErrorCode::Toomanyrequests, e.code);
assert_eq!("pull request limit exceeded", e.message);
assert_ne!(serde_json::value::Value::Null, e.detail);
}
const EXAMPLE_ERROR_MISSING_MESSAGE: &str = r#"
{"errors":[{"code":"UNAUTHORIZED","detail":[{"Type":"repository","Name":"hello-wasm","Action":"pull"}]}]}
"#;
#[test]
fn test_deserialize_without_message_field() {
let envelope: OciEnvelope =
serde_json::from_str(EXAMPLE_ERROR_MISSING_MESSAGE).expect("parse example error");
let e = &envelope.errors[0];
assert_eq!(OciErrorCode::Unauthorized, e.code);
assert_eq!(String::default(), e.message);
assert_ne!(serde_json::value::Value::Null, e.detail);
}
const EXAMPLE_ERROR_MISSING_DETAIL: &str = r#"
{"errors":[{"code":"UNAUTHORIZED","message":"authentication required"}]}
"#;
#[test]
fn test_deserialize_without_detail_field() {
let envelope: OciEnvelope =
serde_json::from_str(EXAMPLE_ERROR_MISSING_DETAIL).expect("parse example error");
let e = &envelope.errors[0];
assert_eq!(OciErrorCode::Unauthorized, e.code);
assert_eq!("authentication required", e.message);
assert_eq!(serde_json::value::Value::Null, e.detail);
}
}