use super::remote_transport::{
InvalidAuthenticationConfiguration, InvalidRemoteResponse, RemoteTransportFailure,
};
use super::ImageRef;
use oci_client::errors::{OciDistributionError, OciErrorCode};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RemoteArtifactError {
#[error("Remote artifact manifest not found: {image}")]
ManifestNotFound {
image: Box<ImageRef>,
#[source]
source: crate::Error,
},
#[error("Failed to authenticate for remote artifact: {image}")]
Authentication {
image: Box<ImageRef>,
#[source]
source: crate::Error,
},
#[error("Access denied for remote artifact: {image}")]
Authorization {
image: Box<ImageRef>,
#[source]
source: crate::Error,
},
#[error("Transport failure while accessing remote artifact: {image}")]
Transport {
image: Box<ImageRef>,
#[source]
source: crate::Error,
},
#[error("Invalid remote artifact: {image}")]
InvalidArtifact {
image: Box<ImageRef>,
#[source]
source: crate::Error,
},
#[error("Failed to access remote artifact: {image}")]
Other {
image: Box<ImageRef>,
#[source]
source: crate::Error,
},
}
pub fn classify_manifest(image: &ImageRef, source: crate::Error) -> RemoteArtifactError {
classify(image, source, RemoteOperation::ManifestLookup)
}
pub fn classify_blob(image: &ImageRef, source: crate::Error) -> RemoteArtifactError {
classify(image, source, RemoteOperation::BlobFetch)
}
fn classify(
image: &ImageRef,
source: crate::Error,
operation: RemoteOperation,
) -> RemoteArtifactError {
let category = if source.chain().any(|cause| {
cause
.downcast_ref::<InvalidAuthenticationConfiguration>()
.is_some()
}) {
Category::Authentication
} else if source
.chain()
.any(|cause| cause.downcast_ref::<InvalidRemoteResponse>().is_some())
{
Category::InvalidArtifact
} else if source
.chain()
.any(|cause| cause.downcast_ref::<RemoteTransportFailure>().is_some())
{
Category::Transport
} else if let Some(error) = source
.chain()
.find_map(|cause| cause.downcast_ref::<OciDistributionError>())
{
classify_oci_error(error, operation)
} else if source
.chain()
.any(|cause| cause.downcast_ref::<serde_json::Error>().is_some())
{
Category::InvalidArtifact
} else {
Category::Other
};
RemoteArtifactError::from_category(image, source, category)
}
pub fn invalid_artifact(image: &ImageRef, source: crate::Error) -> RemoteArtifactError {
RemoteArtifactError::from_category(image, source, Category::InvalidArtifact)
}
impl RemoteArtifactError {
fn from_category(image: &ImageRef, source: crate::Error, category: Category) -> Self {
let image = Box::new(image.clone());
match category {
Category::ManifestNotFound => Self::ManifestNotFound { image, source },
Category::Authentication => Self::Authentication { image, source },
Category::Authorization => Self::Authorization { image, source },
Category::Transport => Self::Transport { image, source },
Category::InvalidArtifact => Self::InvalidArtifact { image, source },
Category::Other => Self::Other { image, source },
}
}
pub fn image(&self) -> &ImageRef {
match self {
Self::ManifestNotFound { image, .. }
| Self::Authentication { image, .. }
| Self::Authorization { image, .. }
| Self::Transport { image, .. }
| Self::InvalidArtifact { image, .. }
| Self::Other { image, .. } => image.as_ref(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Category {
ManifestNotFound,
Authentication,
Authorization,
Transport,
InvalidArtifact,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RemoteOperation {
ManifestLookup,
BlobFetch,
}
fn classify_oci_error(error: &OciDistributionError, operation: RemoteOperation) -> Category {
match error {
OciDistributionError::ImageManifestNotFoundError(_) => absence_category(operation),
OciDistributionError::AuthenticationFailure(_)
| OciDistributionError::ConfigConversionError(_)
| OciDistributionError::RegistryTokenDecodeError(_)
| OciDistributionError::UnauthorizedError { .. } => Category::Authentication,
OciDistributionError::RegistryError { envelope, .. } => {
classify_registry_codes(envelope.errors.iter().map(|error| &error.code), operation)
}
OciDistributionError::RequestError(error) => error
.status()
.map(|status| classify_http_status(status.as_u16(), operation))
.unwrap_or(Category::Transport),
OciDistributionError::IoError(_) => Category::Transport,
OciDistributionError::ServerError { code: 401, .. } => Category::Authentication,
OciDistributionError::ServerError { code: 403, .. } => Category::Authorization,
OciDistributionError::ServerError { code: 404, .. } => absence_category(operation),
OciDistributionError::ServerError { code, .. } if *code >= 500 => Category::Transport,
OciDistributionError::DigestError(_)
| OciDistributionError::HeaderValueError(_)
| OciDistributionError::JsonError(_)
| OciDistributionError::ManifestEncodingError(_)
| OciDistributionError::ManifestParsingError(_)
| OciDistributionError::RegistryNoDigestError
| OciDistributionError::SpecViolationError(_)
| OciDistributionError::UnsupportedMediaTypeError(_)
| OciDistributionError::UnsupportedSchemaVersionError(_)
| OciDistributionError::VersionedParsingError(_)
| OciDistributionError::ImageIndexParsingNoPlatformResolverError
| OciDistributionError::IncompatibleLayerMediaTypeError(_) => Category::InvalidArtifact,
OciDistributionError::GenericError(_)
| OciDistributionError::PushNoDataError
| OciDistributionError::PushLayerNoDataError
| OciDistributionError::PullNoLayersError
| OciDistributionError::RegistryNoLocationError
| OciDistributionError::UrlParseError(_)
| OciDistributionError::ServerError { .. } => Category::Other,
}
}
fn classify_http_status(status: u16, operation: RemoteOperation) -> Category {
match status {
401 => Category::Authentication,
403 => Category::Authorization,
404 => absence_category(operation),
_ => Category::Transport,
}
}
fn absence_category(operation: RemoteOperation) -> Category {
match operation {
RemoteOperation::ManifestLookup => Category::ManifestNotFound,
RemoteOperation::BlobFetch => Category::InvalidArtifact,
}
}
fn classify_registry_codes<'a>(
codes: impl Iterator<Item = &'a OciErrorCode>,
operation: RemoteOperation,
) -> Category {
let codes: Vec<_> = codes.collect();
if codes
.iter()
.any(|code| matches!(code, OciErrorCode::Unauthorized))
{
return Category::Authentication;
}
if codes
.iter()
.any(|code| matches!(code, OciErrorCode::Denied))
{
return Category::Authorization;
}
if !codes.is_empty()
&& codes.iter().all(|code| {
matches!(
code,
OciErrorCode::ManifestUnknown | OciErrorCode::NameUnknown | OciErrorCode::NotFound
)
})
{
return absence_category(operation);
}
if codes.iter().any(|code| {
matches!(
code,
OciErrorCode::BlobUnknown
| OciErrorCode::DigestInvalid
| OciErrorCode::ManifestBlobUnknown
| OciErrorCode::ManifestInvalid
| OciErrorCode::ManifestUnverified
| OciErrorCode::NameInvalid
| OciErrorCode::SizeInvalid
| OciErrorCode::TagInvalid
)
}) {
return Category::InvalidArtifact;
}
Category::Other
}
#[cfg(test)]
mod tests {
use super::*;
use oci_client::errors::{OciEnvelope, OciError};
fn registry_error(codes: impl IntoIterator<Item = OciErrorCode>) -> OciDistributionError {
OciDistributionError::RegistryError {
envelope: OciEnvelope {
errors: codes
.into_iter()
.map(|code| OciError {
code,
message: String::new(),
detail: serde_json::Value::Null,
})
.collect(),
},
url: "https://registry.example/v2/repo/manifests/tag".to_string(),
}
}
#[test]
fn classifies_manifest_absence_without_string_matching() {
assert_eq!(
classify_oci_error(
®istry_error([OciErrorCode::ManifestUnknown]),
RemoteOperation::ManifestLookup,
),
Category::ManifestNotFound
);
assert_eq!(
classify_oci_error(
®istry_error([OciErrorCode::NameUnknown]),
RemoteOperation::ManifestLookup,
),
Category::ManifestNotFound
);
}
#[test]
fn classifies_missing_referenced_blob_as_invalid_artifact() {
assert_eq!(
classify_oci_error(
®istry_error([OciErrorCode::NotFound]),
RemoteOperation::BlobFetch,
),
Category::InvalidArtifact
);
assert_eq!(
classify_oci_error(
®istry_error([OciErrorCode::BlobUnknown]),
RemoteOperation::BlobFetch,
),
Category::InvalidArtifact
);
}
#[test]
fn authentication_and_authorization_take_priority_over_absence() {
assert_eq!(
classify_oci_error(
®istry_error([OciErrorCode::ManifestUnknown, OciErrorCode::Unauthorized,]),
RemoteOperation::BlobFetch,
),
Category::Authentication
);
assert_eq!(
classify_oci_error(
®istry_error([OciErrorCode::Denied]),
RemoteOperation::BlobFetch,
),
Category::Authorization
);
}
#[test]
fn classifies_registry_server_failure_as_transport() {
assert_eq!(
classify_oci_error(
&OciDistributionError::ServerError {
code: 503,
url: "https://registry.example/v2/".to_string(),
message: "unavailable".to_string(),
},
RemoteOperation::ManifestLookup,
),
Category::Transport
);
}
}