-
-
Notifications
You must be signed in to change notification settings - Fork 317
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Unify API error types #450
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
|
||
use derivative::Derivative; | ||
use futures::{stream::BoxStream, Stream, StreamExt}; | ||
use k8s_openapi::apimachinery::pkg::apis::meta::v1::Status; | ||
use kube::{ | ||
api::{ListParams, Meta, WatchEvent}, | ||
Api, | ||
|
@@ -23,11 +24,8 @@ pub enum Error { | |
source: kube::Error, | ||
backtrace: Backtrace, | ||
}, | ||
#[snafu(display("error returned by apiserver during watch: {}", source))] | ||
WatchError { | ||
source: kube::error::ErrorResponse, | ||
backtrace: Backtrace, | ||
}, | ||
#[snafu(display("error returned by apiserver during watch: {:?}", status))] | ||
WatchError { status: Status, backtrace: Backtrace }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is this now the debug print? |
||
#[snafu(display("watch stream failed: {}", source))] | ||
WatchFailed { | ||
source: kube::Error, | ||
|
@@ -153,15 +151,15 @@ async fn step_trampolined<K: Meta + Clone + DeserializeOwned + Send + 'static>( | |
}), | ||
Some(Ok(WatchEvent::Error(err))) => { | ||
// HTTP GONE, means we have desynced and need to start over and re-list :( | ||
let new_state = if err.code == 410 { | ||
let new_state = if err.code == Some(410) { | ||
State::Empty | ||
} else { | ||
State::Watching { | ||
resource_version, | ||
stream, | ||
} | ||
}; | ||
(Some(Err(err).context(WatchError)), new_state) | ||
(Some(WatchError { status: err }.fail()), new_state) | ||
} | ||
Some(Err(err)) => (Some(Err(err).context(WatchFailed)), State::Watching { | ||
resource_version, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,6 @@ | |
use crate::{ | ||
api::{Meta, WatchEvent}, | ||
config::Config, | ||
error::ErrorResponse, | ||
service::Service, | ||
Error, Result, | ||
}; | ||
|
@@ -22,8 +21,8 @@ use either::{Either, Left, Right}; | |
use futures::{self, Stream, StreamExt, TryStream, TryStreamExt}; | ||
use http::{self, Request, Response, StatusCode}; | ||
use hyper::Body; | ||
use k8s_openapi::apimachinery::pkg::apis::meta::v1 as k8s_meta_v1; | ||
use serde::{de::DeserializeOwned, Deserialize}; | ||
use k8s_openapi::apimachinery::pkg::apis::meta::v1::{self as k8s_meta_v1, Status}; | ||
use serde::de::DeserializeOwned; | ||
use serde_json::{self, Value}; | ||
use tokio_util::{ | ||
codec::{FramedRead, LinesCodec, LinesCodecError}, | ||
|
@@ -239,7 +238,7 @@ impl Client { | |
} | ||
|
||
// Got general error response | ||
if let Ok(e_resp) = serde_json::from_str::<ErrorResponse>(&line) { | ||
if let Ok(e_resp) = serde_json::from_str::<Status>(&line) { | ||
return Some(Err(Error::Api(e_resp))); | ||
} | ||
// Parsing error | ||
|
@@ -328,17 +327,19 @@ fn handle_api_errors(text: &str, s: StatusCode) -> Result<()> { | |
if s.is_client_error() || s.is_server_error() { | ||
// Print better debug when things do fail | ||
// trace!("Parsing error: {}", text); | ||
if let Ok(errdata) = serde_json::from_str::<ErrorResponse>(text) { | ||
if let Ok(errdata) = serde_json::from_str::<Status>(text) { | ||
debug!("Unsuccessful: {:?}", errdata); | ||
Err(Error::Api(errdata)) | ||
} else { | ||
warn!("Unsuccessful data error parse: {}", text); | ||
// Propagate errors properly via reqwest | ||
let ae = ErrorResponse { | ||
status: s.to_string(), | ||
code: s.as_u16(), | ||
message: format!("{:?}", text), | ||
reason: "Failed to parse error data".into(), | ||
let ae = Status { | ||
metadata: Default::default(), | ||
status: Some(s.to_string()), | ||
code: Some(s.as_u16().into()), | ||
message: Some(format!("{:?}", text)), | ||
reason: Some("Failed to parse error data".into()), | ||
details: None, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This now causes a lot more option types that always exist (like most k8s-openapi structs), and that was the main reason for the separation of the structs. |
||
}; | ||
debug!("Unsuccessful: {:?} (reconstruct)", ae); | ||
Err(Error::Api(ae)) | ||
|
@@ -357,74 +358,6 @@ impl TryFrom<Config> for Client { | |
} | ||
} | ||
|
||
// TODO: replace with Status in k8s openapi? | ||
|
||
/// A Kubernetes status object | ||
#[allow(missing_docs)] | ||
#[derive(Deserialize, Debug)] | ||
pub struct Status { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At least we are getting rid of these duplicate structs. |
||
// TODO: typemeta | ||
// TODO: metadata that can be completely empty (listmeta...) | ||
#[serde(default, skip_serializing_if = "String::is_empty")] | ||
pub status: String, | ||
#[serde(default, skip_serializing_if = "String::is_empty")] | ||
pub message: String, | ||
#[serde(default, skip_serializing_if = "String::is_empty")] | ||
pub reason: String, | ||
#[serde(default, skip_serializing_if = "Option::is_none")] | ||
pub details: Option<StatusDetails>, | ||
#[serde(default, skip_serializing_if = "num::Zero::is_zero")] | ||
pub code: u16, | ||
} | ||
|
||
/// Status details object on the [`Status`] object | ||
#[derive(Deserialize, Debug)] | ||
#[serde(rename_all = "camelCase")] | ||
#[allow(missing_docs)] | ||
pub struct StatusDetails { | ||
#[serde(default, skip_serializing_if = "String::is_empty")] | ||
pub name: String, | ||
#[serde(default, skip_serializing_if = "String::is_empty")] | ||
pub group: String, | ||
#[serde(default, skip_serializing_if = "String::is_empty")] | ||
pub kind: String, | ||
#[serde(default, skip_serializing_if = "String::is_empty")] | ||
pub uid: String, | ||
#[serde(default, skip_serializing_if = "Vec::is_empty")] | ||
pub causes: Vec<StatusCause>, | ||
#[serde(default, skip_serializing_if = "num::Zero::is_zero")] | ||
pub retry_after_seconds: u32, | ||
} | ||
|
||
/// Status cause object on the [`StatusDetails`] object | ||
#[derive(Deserialize, Debug)] | ||
#[allow(missing_docs)] | ||
pub struct StatusCause { | ||
#[serde(default, skip_serializing_if = "String::is_empty")] | ||
pub reason: String, | ||
#[serde(default, skip_serializing_if = "String::is_empty")] | ||
pub message: String, | ||
#[serde(default, skip_serializing_if = "String::is_empty")] | ||
pub field: String, | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::Status; | ||
|
||
// ensure our status schema is sensible | ||
#[test] | ||
fn delete_deserialize_test() { | ||
let statusresp = r#"{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Success","details":{"name":"some-app","group":"clux.dev","kind":"foos","uid":"1234-some-uid"}}"#; | ||
let s: Status = serde_json::from_str::<Status>(statusresp).unwrap(); | ||
assert_eq!(s.details.unwrap().name, "some-app"); | ||
|
||
let statusnoname = r#"{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Success","details":{"group":"clux.dev","kind":"foos","uid":"1234-some-uid"}}"#; | ||
let s2: Status = serde_json::from_str::<Status>(statusnoname).unwrap(); | ||
assert_eq!(s2.details.unwrap().name, ""); // optional probably better.. | ||
} | ||
} | ||
|
||
#[cfg(feature = "ws")] | ||
// Verify upgrade response according to RFC6455. | ||
// Based on `tungstenite` and added subprotocol verification. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
//! Error handling in [`kube`][crate] | ||
|
||
use http::header::InvalidHeaderValue; | ||
use serde::{Deserialize, Serialize}; | ||
use k8s_openapi::apimachinery::pkg::apis::meta::v1::Status; | ||
use std::path::PathBuf; | ||
use thiserror::Error; | ||
|
||
|
@@ -14,8 +14,8 @@ pub enum Error { | |
/// It's also used in `WatchEvent` from watch calls. | ||
/// | ||
/// It's quite common to get a `410 Gone` when the `resourceVersion` is too old. | ||
#[error("ApiError: {0} ({0:?})")] | ||
Api(#[source] ErrorResponse), | ||
#[error("ApiError: {0:?}")] | ||
Api(Status), | ||
|
||
/// ConnectionError for when TcpStream fails to connect. | ||
#[error("ConnectionError: {0}")] | ||
|
@@ -261,19 +261,3 @@ impl From<OAuthError> for Error { | |
ConfigError::OAuth(e).into() | ||
} | ||
} | ||
|
||
/// An error response from the API. | ||
#[derive(Error, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] | ||
#[error("{message}: {reason}")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason for expanding the nice shorthand to the full debug print? |
||
pub struct ErrorResponse { | ||
/// The status | ||
pub status: String, | ||
/// A message about the error | ||
#[serde(default)] | ||
pub message: String, | ||
/// The reason for the error | ||
#[serde(default)] | ||
pub reason: String, | ||
/// The error code | ||
pub code: u16, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a bit uglier, and a breaking change, but a small one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, API error handling already felt awkward when writing controllers. Matches like
Err(kube::Error::Api(kube::error::ErrorResponse { code: 404, .. }))
are common.I was going to propose switching to
snafu
and add newError
specific for client/API (let's saykube::client::Error
for now). Providing specific variants likeError::NotFound
andError::Conflict
makes sense because these are very common. We can include our wrapper formeta::v1::Status
in variants for those who need them.Also, I'm not sure if there's any reason to keep the deprecated runtime, but I think it makes sense to move
kube-runtime
underkube
soon and this (snafu
) is a necessary step.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, there's definitely bigger improvements to be made in client error handling, separating it out more like how it's done for
ConfigError
, and special purpose creating specific errors would help a lot like you say.A small question though. How are you finding working with
snafu
? What benefits do you find with it overthiserror
?I ask because I was kind of thinking to propose unifying under
thiserror
instead because every time i make another app, I have started reaching for it instead because of ergonomics:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, the big benefit is that the context selectors push you towards having many smaller and more specific error types that act as a sort of semantic backtrace ("file not found while finding Y while reconciling X"), rather than the more or less unmanageable
kube::Error
("something has gone wrong in library X, have fun figuring out why").snafu
is agnostic to struct vs tuple variants, you can do:The current state in
kube-derive
is more about my own bias towards struct-style enums.Backtraces can also be inherited from other
snafu
errors, which would save you an item.Not really, if you follow
snafu
's recommended error-enum-per-module style. The enum is the external interface of the module, the context selectors are an implementation detail.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move to #453? It's easier for others to join, and we can leave this PR alone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that that would be more comfortable, but it's a big compatibility footgun, sadly.
Adding a
Conflict
arm would mean that a match onErr(kube::Error::Api(kube::error::ErrorResponse { code: 409, .. }))
would still compile just fine, but silently never match anything.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we keep
kube::client::Status
, we can add something like:Otherwise, it can be implemented as a free function or an extension trait for
k8s_openapi::Status