Skip to content
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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

Unify API error types #450

wants to merge 3 commits into from

Conversation

MikailBag
Copy link
Contributor

Currently kube has two similar structs that are intended to hold API errors - Status and ErrorResponse. However, both types are incomplete. This pull request deletes both structs in favor of metav1::Status defined by k8s-openapi.

@MikailBag
Copy link
Contributor Author

MikailBag commented Mar 5, 2021

Alternatively, we can replace ErrorResponse with Status. This way we can still provide impl Error.

Copy link
Member

@clux clux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in general this is reasonable, and it uncouples us a bit more from kubernetes code.

It's a bit awkward because the openapi types are a lot more option heavy and this is making the error interface arguably uglier, but it is unifying the types, which is probably the smarter thing to do here.

Left one question on the debug print.

code: Some(s.as_u16().into()),
message: Some(format!("{:?}", text)),
reason: Some("Failed to parse error data".into()),
details: None,
Copy link
Member

Choose a reason for hiding this comment

The 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.

/// A Kubernetes status object
#[allow(missing_docs)]
#[derive(Deserialize, Debug)]
pub struct Status {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least we are getting rid of these duplicate structs.

backtrace: Backtrace,
},
#[snafu(display("error returned by apiserver during watch: {:?}", status))]
WatchError { status: Status, backtrace: Backtrace },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this now the debug print?

@@ -41,8 +41,8 @@ async fn main() -> anyhow::Result<()> {
// wait for it..
std::thread::sleep(std::time::Duration::from_millis(5_000));
}
Err(kube::Error::Api(ae)) => assert_eq!(ae.code, 409), // if you skipped delete, for instance
Err(e) => return Err(e.into()), // any other case is probably bad
Err(kube::Error::Api(ae)) => assert_eq!(ae.code, Some(409)), // if you skipped delete, for instance
Copy link
Member

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.

Copy link
Member

@kazk kazk Mar 8, 2021

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 new Error specific for client/API (let's say kube::client::Error for now). Providing specific variants like Error::NotFound and Error::Conflict makes sense because these are very common. We can include our wrapper for meta::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 under kube soon and this (snafu) is a necessary step.

Copy link
Member

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 over thiserror?

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:

  • snafu's error enum ends up being 5 lines per impl (when using a source + backtrace) rather than thiserror's 2
  • snafu's error variants all have to be imported into scope and causes extra cycles when developing

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small question though. How are you finding working with snafu? What benefits do you find with it over thiserror?

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's error enum ends up being 5 lines per impl (when using a source + backtrace) rather than thiserror's 2

snafu is agnostic to struct vs tuple variants, you can do:

#[derive(Debug, Snafu)
pub enum Error {
    #[snafu(display("IO error"))]
    Io(#[snafu(source)] std::io::Error, #[snafu(backtrace)] Backtrace)
}

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.

  • snafu's error variants all have to be imported into scope and causes extra cycles when developing

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.

Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Providing specific variants like Error::NotFound and Error::Conflict makes sense because these are very common. We can include our wrapper for meta::v1::Status in variants for those who need them.

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 on Err(kube::Error::Api(kube::error::ErrorResponse { code: 409, .. })) would still compile just fine, but silently never match anything.

Copy link
Contributor Author

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:

impl Status {
    pub fn classify(&self) -> Reason;
}

#[non_exhaustive]
pub enum Reason {
    NotExists,
    Confict,
    PermissionDenied,
    Unknown
}

Otherwise, it can be implemented as a free function or an extension trait for k8s_openapi::Status


/// An error response from the API.
#[derive(Error, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
#[error("{message}: {reason}")]
Copy link
Member

Choose a reason for hiding this comment

The 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?

@MikailBag
Copy link
Contributor Author

Status from k8s-openapi does not implement Display.

What do you think about alternative?

  • Verify that kube::client::Status has all fields that metav1::Status has.
  • Replace ErrorResponse with kube::client::Status

cons:

  • we have hard-coded version of k8s-openapi stuff (but it's still better than current situation when we have two versions)

pros:

  • less Options
  • Display implementation
  • Error implementation

Based on your review I now think this approach is better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants