use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum KubeError {
#[error("network error: {0}")]
Network(String),
#[error("apiserver status {code} {kind:?}: {message}")]
ApiStatus {
code: u16,
kind: ApiStatusKind,
message: String,
},
#[error("decode error: {0}")]
Decode(String),
#[error("encode error: {0}")]
Encode(String),
#[error("auth error: {0}")]
Auth(String),
#[error("watch closed by server")]
WatchClosed,
#[error("resourceVersion {0} expired — relist required")]
ResourceVersionExpired(String),
#[error("not found: {0}")]
NotFound(String),
#[error("{0}")]
Other(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApiStatusKind {
Unauthorized,
Forbidden,
NotFound,
Conflict,
AlreadyExists,
Gone,
Invalid,
TooManyRequests,
InternalError,
ServiceUnavailable,
Timeout,
Other,
}
impl ApiStatusKind {
#[must_use]
pub fn from_code(code: u16) -> Self {
match code {
401 => Self::Unauthorized,
403 => Self::Forbidden,
404 => Self::NotFound,
409 => Self::Conflict,
410 => Self::Gone,
422 => Self::Invalid,
429 => Self::TooManyRequests,
500 => Self::InternalError,
503 => Self::ServiceUnavailable,
504 => Self::Timeout,
_ => Self::Other,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FailureClass {
Transient,
Declarative,
}
impl KubeError {
#[must_use]
pub fn classify(&self) -> FailureClass {
match self {
Self::Network(_)
| Self::WatchClosed
| Self::ResourceVersionExpired(_) => FailureClass::Transient,
Self::ApiStatus { kind, .. } => match kind {
ApiStatusKind::Conflict
| ApiStatusKind::TooManyRequests
| ApiStatusKind::InternalError
| ApiStatusKind::ServiceUnavailable
| ApiStatusKind::Timeout
| ApiStatusKind::Gone => FailureClass::Transient,
_ => FailureClass::Declarative,
},
Self::NotFound(_)
| Self::Decode(_)
| Self::Encode(_)
| Self::Auth(_)
| Self::Other(_) => FailureClass::Declarative,
}
}
#[must_use]
pub fn retry_after(&self) -> Option<Duration> {
match self {
Self::Network(_) => Some(Duration::from_secs(1)),
Self::WatchClosed | Self::ResourceVersionExpired(_) => {
Some(Duration::from_millis(100))
}
Self::ApiStatus { kind, .. } => match kind {
ApiStatusKind::Conflict => Some(Duration::ZERO),
ApiStatusKind::TooManyRequests => Some(Duration::from_secs(5)),
ApiStatusKind::InternalError
| ApiStatusKind::ServiceUnavailable
| ApiStatusKind::Timeout
| ApiStatusKind::Gone => Some(Duration::from_secs(1)),
_ => None,
},
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn network_error_is_transient() {
let e = KubeError::Network("connection refused".into());
assert_eq!(e.classify(), FailureClass::Transient);
assert_eq!(e.retry_after(), Some(Duration::from_secs(1)));
}
#[test]
fn auth_error_is_declarative() {
let e = KubeError::Auth("kubeconfig parse failed".into());
assert_eq!(e.classify(), FailureClass::Declarative);
assert_eq!(e.retry_after(), None);
}
#[test]
fn conflict_is_immediate_retry() {
let e = KubeError::ApiStatus {
code: 409,
kind: ApiStatusKind::Conflict,
message: "resourceVersion mismatch".into(),
};
assert_eq!(e.classify(), FailureClass::Transient);
assert_eq!(e.retry_after(), Some(Duration::ZERO));
}
#[test]
fn forbidden_is_declarative() {
let e = KubeError::ApiStatus {
code: 403,
kind: ApiStatusKind::Forbidden,
message: "RBAC denied".into(),
};
assert_eq!(e.classify(), FailureClass::Declarative);
}
#[test]
fn too_many_requests_backs_off_5s() {
let e = KubeError::ApiStatus {
code: 429,
kind: ApiStatusKind::TooManyRequests,
message: "calm down".into(),
};
assert_eq!(e.retry_after(), Some(Duration::from_secs(5)));
}
#[test]
fn status_kind_from_code_covers_canonical() {
assert_eq!(ApiStatusKind::from_code(401), ApiStatusKind::Unauthorized);
assert_eq!(ApiStatusKind::from_code(403), ApiStatusKind::Forbidden);
assert_eq!(ApiStatusKind::from_code(404), ApiStatusKind::NotFound);
assert_eq!(ApiStatusKind::from_code(409), ApiStatusKind::Conflict);
assert_eq!(ApiStatusKind::from_code(410), ApiStatusKind::Gone);
assert_eq!(ApiStatusKind::from_code(422), ApiStatusKind::Invalid);
assert_eq!(ApiStatusKind::from_code(429), ApiStatusKind::TooManyRequests);
assert_eq!(ApiStatusKind::from_code(500), ApiStatusKind::InternalError);
assert_eq!(ApiStatusKind::from_code(503), ApiStatusKind::ServiceUnavailable);
assert_eq!(ApiStatusKind::from_code(504), ApiStatusKind::Timeout);
assert_eq!(ApiStatusKind::from_code(418), ApiStatusKind::Other);
}
}