kube_core/
response.rs

1//! Generic api response types
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4
5/// A Kubernetes status object
6///
7/// This struct is returned by the Kubernetes API on failures,
8/// and bubbles up to users inside a [`kube::Error::Api`] variant
9/// when client requests fail in [`kube::Client`].
10///
11/// To match on specific error cases, you can;
12///
13/// ```no_compile
14/// match err {
15///     kube::Error::Api(s) if s.is_not_found() => {...},
16/// }
17/// ```
18///
19/// or in a standalone `if` statement with [std::matches];
20///
21/// ```no_compile
22/// if std::matches!(err, kube::Error::Api(s) if s.is_forbidden()) {...}
23/// ```
24#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Error)]
25#[error("{message}: {reason}")]
26pub struct Status {
27    /// Status of the operation
28    ///
29    /// One of: `Success` or `Failure` - [more info](https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status)
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub status: Option<StatusSummary>,
32
33    /// Suggested HTTP return code (0 if unset)
34    #[serde(default, skip_serializing_if = "is_u16_zero")]
35    pub code: u16,
36
37    /// A human-readable  description of the status of this operation
38    #[serde(default, skip_serializing_if = "String::is_empty")]
39    pub message: String,
40
41    /// Standard list metadata - [more info](https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds)
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub metadata: Option<k8s_openapi::apimachinery::pkg::apis::meta::v1::ListMeta>,
44
45    /// A machine-readable description of why this operation is in the “Failure” status.
46    ///
47    /// If this value is empty there is no information available.
48    /// A Reason clarifies an HTTP status code but does not override it.
49    #[serde(default, skip_serializing_if = "String::is_empty")]
50    pub reason: String,
51
52    /// Extended data associated with the reason.
53    ///
54    /// Each reason may define its own extended details.
55    /// This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub details: Option<StatusDetails>,
58}
59
60impl Status {
61    /// Returns a boxed `Status`
62    pub fn boxed(self) -> Box<Self> {
63        Box::new(self)
64    }
65
66    /// Returns a successful `Status`
67    pub fn success() -> Self {
68        Status {
69            status: Some(StatusSummary::Success),
70            code: 0,
71            message: String::new(),
72            metadata: None,
73            reason: String::new(),
74            details: None,
75        }
76    }
77
78    /// Returns an unsuccessful `Status`
79    pub fn failure(message: &str, reason: &str) -> Self {
80        Status {
81            status: Some(StatusSummary::Failure),
82            code: 0,
83            message: message.to_string(),
84            metadata: None,
85            reason: reason.to_string(),
86            details: None,
87        }
88    }
89
90    /// Sets an explicit HTTP status code
91    pub fn with_code(mut self, code: u16) -> Self {
92        self.code = code;
93        self
94    }
95
96    /// Adds details to the `Status`
97    pub fn with_details(mut self, details: StatusDetails) -> Self {
98        self.details = Some(details);
99        self
100    }
101
102    /// Checks if this `Status` represents success
103    ///
104    /// Note that it is possible for `Status` to be in indeterminate state
105    /// when both `is_success` and `is_failure` return false.
106    pub fn is_success(&self) -> bool {
107        self.status == Some(StatusSummary::Success)
108    }
109
110    /// Checks if this `Status` represents failure
111    ///
112    /// Note that it is possible for `Status` to be in indeterminate state
113    /// when both `is_success` and `is_failure` return false.
114    pub fn is_failure(&self) -> bool {
115        self.status == Some(StatusSummary::Failure)
116    }
117
118    /// Checks if this `Status` represents not found error
119    ///
120    /// Note that it is possible for `Status` to be in indeterminate state
121    /// when both `is_success` and `is_failure` return false.
122    pub fn is_not_found(&self) -> bool {
123        self.reason_or_code(reason::NOT_FOUND, 404)
124    }
125
126    /// Checks if this `Status` indicates that a specified resource already exists.
127    pub fn is_already_exists(&self) -> bool {
128        self.reason == reason::ALREADY_EXISTS
129    }
130
131    /// Checks if this `Status` indicates update conflict
132    pub fn is_conflict(&self) -> bool {
133        self.reason_or_code(reason::CONFLICT, 409)
134    }
135
136    /// Checks if this `Status` indicates that the request is forbidden and cannot
137    /// be completed as requested.
138    pub fn is_forbidden(&self) -> bool {
139        self.reason_or_code(reason::FORBIDDEN, 403)
140    }
141
142    /// Checks if this `Status` indicates that provided resource is not valid.
143    pub fn is_invalid(&self) -> bool {
144        self.reason_or_code(reason::INVALID, 422)
145    }
146
147    // This helper function is used by other is_xxx helpers.
148    // Its implementation follows that of the Go client.
149    // See for example
150    // https://github.com/kubernetes/apimachinery/blob/v0.35.0/pkg/api/errors/errors.go#L529
151    fn reason_or_code(&self, reason: &str, code: u16) -> bool {
152        self.reason == reason || (!reason::is_known(reason) && self.code == code)
153    }
154}
155
156/// Overall status of the operation - whether it succeeded or not
157#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
158pub enum StatusSummary {
159    /// Operation succeeded
160    Success,
161    /// Operation failed
162    Failure,
163}
164
165/// Status details object on the [`Status`] object
166#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
167#[serde(rename_all = "camelCase")]
168pub struct StatusDetails {
169    /// The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described)
170    #[serde(default, skip_serializing_if = "String::is_empty")]
171    pub name: String,
172
173    /// The group attribute of the resource associated with the status StatusReason
174    #[serde(default, skip_serializing_if = "String::is_empty")]
175    pub group: String,
176
177    /// The kind attribute of the resource associated with the status StatusReason
178    ///
179    /// On some operations may differ from the requested resource Kind - [more info](https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds)
180    #[serde(default, skip_serializing_if = "String::is_empty")]
181    pub kind: String,
182
183    /// UID of the resource (when there is a single resource which can be described)
184    ///
185    /// [More info](http://kubernetes.io/docs/user-guide/identifiers#uids)
186    #[serde(default, skip_serializing_if = "String::is_empty")]
187    pub uid: String,
188
189    /// The Causes vector includes more details associated with the failure
190    ///
191    /// Not all StatusReasons may provide detailed causes.
192    #[serde(default, skip_serializing_if = "Vec::is_empty")]
193    pub causes: Vec<StatusCause>,
194
195    /// If specified, the time in seconds before the operation should be retried.
196    ///
197    /// Some errors may indicate the client must take an alternate action -
198    /// for those errors this field may indicate how long to wait before taking the alternate action.
199    #[serde(default, skip_serializing_if = "is_u32_zero")]
200    pub retry_after_seconds: u32,
201}
202
203/// Status cause object on the [`StatusDetails`] object
204#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
205pub struct StatusCause {
206    /// A machine-readable description of the cause of the error. If this value is empty there is no information available.
207    #[serde(default, skip_serializing_if = "String::is_empty")]
208    pub reason: String,
209
210    /// A human-readable description of the cause of the error. This field may be presented as-is to a reader.
211    #[serde(default, skip_serializing_if = "String::is_empty")]
212    pub message: String,
213
214    /// The field of the resource that has caused this error, as named by its JSON serialization
215    ///
216    /// May include dot and postfix notation for nested attributes. Arrays are zero-indexed.
217    /// Fields may appear more than once in an array of causes due to fields having multiple errors.
218    #[serde(default, skip_serializing_if = "String::is_empty")]
219    pub field: String,
220}
221
222fn is_u16_zero(&v: &u16) -> bool {
223    v == 0
224}
225
226fn is_u32_zero(&v: &u32) -> bool {
227    v == 0
228}
229
230/// StatusReason is an enumeration of possible failure causes.  Each StatusReason
231/// must map to a single HTTP status code, but multiple reasons may map
232/// to the same HTTP status code.
233///
234/// See https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#StatusReason
235/// for the authoritative list of reasons in Go universe.
236pub mod reason {
237
238    /// StatusReasonUnknown means the server has declined to indicate a specific reason.
239    /// The details field may contain other information about this error.
240    /// Status code 500.
241    pub const UNKNOWN: &str = "";
242
243    /// StatusReasonUnauthorized means the server can be reached and understood the request, but requires
244    /// the user to present appropriate authorization credentials (identified by the WWW-Authenticate header)
245    /// in order for the action to be completed. If the user has specified credentials on the request, the
246    /// server considers them insufficient.
247    /// Status code 401
248    pub const UNAUTHORIZED: &str = "Unauthorized";
249
250    /// StatusReasonForbidden means the server can be reached and understood the request, but refuses
251    /// to take any further action.  It is the result of the server being configured to deny access for some reason
252    /// to the requested resource by the client.
253    /// Details (optional):
254    ///   "kind" string - the kind attribute of the forbidden resource
255    ///                   on some operations may differ from the requested
256    ///                   resource.
257    ///   "id"   string - the identifier of the forbidden resource
258    /// Status code 403
259    pub const FORBIDDEN: &str = "Forbidden";
260
261    /// StatusReasonNotFound means one or more resources required for this operation
262    /// could not be found.
263    /// Details (optional):
264    ///   "kind" string - the kind attribute of the missing resource
265    ///                   on some operations may differ from the requested
266    ///                   resource.
267    ///   "id"   string - the identifier of the missing resource
268    /// Status code 404
269    pub const NOT_FOUND: &str = "NotFound";
270
271    /// StatusReasonAlreadyExists means the resource you are creating already exists.
272    /// Details (optional):
273    ///   "kind" string - the kind attribute of the conflicting resource
274    ///   "id"   string - the identifier of the conflicting resource
275    /// Status code 409
276    pub const ALREADY_EXISTS: &str = "AlreadyExists";
277
278    /// StatusReasonConflict means the requested operation cannot be completed
279    /// due to a conflict in the operation. The client may need to alter the
280    /// request. Each resource may define custom details that indicate the
281    /// nature of the conflict.
282    /// Status code 409
283    pub const CONFLICT: &str = "Conflict";
284
285    /// StatusReasonGone means the item is no longer available at the server and no
286    /// forwarding address is known.
287    /// Status code 410
288    pub const GONE: &str = "Gone";
289
290    /// StatusReasonInvalid means the requested create or update operation cannot be
291    /// completed due to invalid data provided as part of the request. The client may
292    /// need to alter the request. When set, the client may use the StatusDetails
293    /// message field as a summary of the issues encountered.
294    /// Details (optional):
295    ///   "kind" string - the kind attribute of the invalid resource
296    ///   "id"   string - the identifier of the invalid resource
297    ///   "causes"      - one or more StatusCause entries indicating the data in the
298    ///                   provided resource that was invalid.  The code, message, and
299    ///                   field attributes will be set.
300    /// Status code 422
301    pub const INVALID: &str = "Invalid";
302
303    /// StatusReasonServerTimeout means the server can be reached and understood the request,
304    /// but cannot complete the action in a reasonable time. The client should retry the request.
305    /// This is may be due to temporary server load or a transient communication issue with
306    /// another server. Status code 500 is used because the HTTP spec provides no suitable
307    /// server-requested client retry and the 5xx class represents actionable errors.
308    /// Details (optional):
309    ///   "kind" string - the kind attribute of the resource being acted on.
310    ///   "id"   string - the operation that is being attempted.
311    ///   "retryAfterSeconds" int32 - the number of seconds before the operation should be retried
312    /// Status code 500
313    pub const SERVER_TIMEOUT: &str = "ServerTimeout";
314
315    /// StatusReasonStoreReadError means that the server encountered an error while
316    /// retrieving resources from the backend object store.
317    /// This may be due to backend database error, or because processing of the read
318    /// resource failed.
319    /// Details:
320    ///   "kind" string - the kind attribute of the resource being acted on.
321    ///   "name" string - the prefix where the reading error(s) occurred
322    ///   "causes" []StatusCause
323    ///      - (optional):
324    ///        - "type" CauseType - CauseTypeUnexpectedServerResponse
325    ///        - "message" string - the error message from the store backend
326    ///        - "field" string - the full path with the key of the resource that failed reading
327    ///
328    /// Status code 500
329    pub const STORAGE_READ_ERROR: &str = "StorageReadError";
330
331    /// StatusReasonTimeout means that the request could not be completed within the given time.
332    /// Clients can get this response only when they specified a timeout param in the request,
333    /// or if the server cannot complete the operation within a reasonable amount of time.
334    /// The request might succeed with an increased value of timeout param. The client *should*
335    /// wait at least the number of seconds specified by the retryAfterSeconds field.
336    /// Details (optional):
337    ///   "retryAfterSeconds" int32 - the number of seconds before the operation should be retried
338    /// Status code 504
339    pub const TIMEOUT: &str = "Timeout";
340
341    /// StatusReasonTooManyRequests means the server experienced too many requests within a
342    /// given window and that the client must wait to perform the action again. A client may
343    /// always retry the request that led to this error, although the client should wait at least
344    /// the number of seconds specified by the retryAfterSeconds field.
345    /// Details (optional):
346    ///   "retryAfterSeconds" int32 - the number of seconds before the operation should be retried
347    /// Status code 429
348    pub const TOO_MANY_REQUESTS: &str = "TooManyRequests";
349
350    /// StatusReasonBadRequest means that the request itself was invalid, because the request
351    /// doesn't make any sense, for example deleting a read-only object.  This is different than
352    /// StatusReasonInvalid above which indicates that the API call could possibly succeed, but the
353    /// data was invalid.  API calls that return BadRequest can never succeed.
354    /// Status code 400
355    pub const BAD_REQUEST: &str = "BadRequest";
356
357    /// StatusReasonMethodNotAllowed means that the action the client attempted to perform on the
358    /// resource was not supported by the code - for instance, attempting to delete a resource that
359    /// can only be created. API calls that return MethodNotAllowed can never succeed.
360    /// Status code 405
361    pub const METHOD_NOT_ALLOWED: &str = "MethodNotAllowed";
362
363    /// StatusReasonNotAcceptable means that the accept types indicated by the client were not acceptable
364    /// to the server - for instance, attempting to receive protobuf for a resource that supports only json and yaml.
365    /// API calls that return NotAcceptable can never succeed.
366    /// Status code 406
367    pub const NOT_ACCEPTABLE: &str = "NotAcceptable";
368
369    /// StatusReasonRequestEntityTooLarge means that the request entity is too large.
370    /// Status code 413
371    pub const REQUEST_ENTITY_TOO_LARGE: &str = "RequestEntityTooLarge";
372
373    /// StatusReasonUnsupportedMediaType means that the content type sent by the client is not acceptable
374    /// to the server - for instance, attempting to send protobuf for a resource that supports only json and yaml.
375    /// API calls that return UnsupportedMediaType can never succeed.
376    /// Status code 415
377    pub const UNSUPPORTED_MEDIA_TYPE: &str = "UnsupportedMediaType";
378
379    /// StatusReasonInternalError indicates that an internal error occurred, it is unexpected
380    /// and the outcome of the call is unknown.
381    /// Details (optional):
382    ///   "causes" - The original error
383    /// Status code 500
384    pub const INTERNAL_ERROR: &str = "InternalError";
385
386    /// StatusReasonExpired indicates that the request is invalid because the content you are requesting
387    /// has expired and is no longer available. It is typically associated with watches that can't be
388    /// serviced.
389    /// Status code 410 (gone)
390    pub const EXPIRED: &str = "Expired";
391
392    /// StatusReasonServiceUnavailable means that the request itself was valid,
393    /// but the requested service is unavailable at this time.
394    /// Retrying the request after some time might succeed.
395    /// Status code 503
396    pub const SERVICE_UNAVAILABLE: &str = "ServiceUnavailable";
397
398    /// Checks status reason to be one of the known reasons.
399    pub fn is_known(reason: &str) -> bool {
400        KNOWN_REASONS.contains(&reason)
401    }
402
403    const KNOWN_REASONS: &[&str] = &[
404        UNAUTHORIZED,
405        FORBIDDEN,
406        NOT_FOUND,
407        ALREADY_EXISTS,
408        CONFLICT,
409        GONE,
410        INVALID,
411        SERVER_TIMEOUT,
412        STORAGE_READ_ERROR,
413        TIMEOUT,
414        TOO_MANY_REQUESTS,
415        BAD_REQUEST,
416        METHOD_NOT_ALLOWED,
417        NOT_ACCEPTABLE,
418        REQUEST_ENTITY_TOO_LARGE,
419        UNSUPPORTED_MEDIA_TYPE,
420        INTERNAL_ERROR,
421        EXPIRED,
422        SERVICE_UNAVAILABLE,
423    ];
424}
425
426#[cfg(test)]
427mod test {
428
429    use super::*;
430
431    // ensure our status schema is sensible
432    #[test]
433    fn delete_deserialize_test() {
434        let statusresp = r#"{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Success","details":{"name":"some-app","group":"clux.dev","kind":"foos","uid":"1234-some-uid"}}"#;
435        let s: Status = serde_json::from_str::<Status>(statusresp).unwrap();
436        assert_eq!(s.details.unwrap().name, "some-app");
437
438        let statusnoname = r#"{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Success","details":{"group":"clux.dev","kind":"foos","uid":"1234-some-uid"}}"#;
439        let s2: Status = serde_json::from_str::<Status>(statusnoname).unwrap();
440        assert_eq!(s2.details.unwrap().name, ""); // optional probably better..
441    }
442
443    #[test]
444    fn feature_with_details1() {
445        let status = r#"
446            {
447                "kind": "Status",
448                "apiVersion": "v1",
449                "metadata": {},
450                "status": "Failure",
451                "message": "leases.coordination.k8s.io \"test\" is invalid: metadata.resourceVersion: Invalid value: 0: must be specified for an update",
452                "reason": "Invalid",
453                "details": {
454                    "name": "test",
455                    "group": "coordination.k8s.io",
456                    "kind": "leases",
457                    "causes": [
458                    {
459                        "reason": "FieldValueInvalid",
460                        "message": "Invalid value: 0: must be specified for an update",
461                        "field": "metadata.resourceVersion"
462                    }
463                    ]
464                },
465                "code": 422
466            }"#;
467        let s = serde_json::from_str::<Status>(status).unwrap();
468        assert!(s.is_invalid());
469        assert_eq!(s.status.unwrap(), StatusSummary::Failure);
470        assert_eq!(s.details.unwrap().name, "test");
471    }
472
473    #[test]
474    fn failure_with_details2() {
475        let status = r#"
476            {
477              "kind": "Status",
478              "apiVersion": "v1",
479              "metadata": {},
480              "status": "Failure",
481              "message": "Lease.coordination.k8s.io \"test_\" is invalid: metadata.name: Invalid value: \"test_\": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')",
482              "reason": "Invalid",
483              "details": {
484                "name": "test_",
485                "group": "coordination.k8s.io",
486                "kind": "Lease",
487                "causes": [
488                  {
489                    "reason": "FieldValueInvalid",
490                    "message": "Invalid value: \"test_\": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')",
491                    "field": "metadata.name"
492                  }
493                ]
494              },
495              "code": 422
496            }"#;
497        let s = serde_json::from_str::<Status>(status).unwrap();
498        assert!(s.is_invalid());
499        assert_eq!(s.status.unwrap(), StatusSummary::Failure);
500        assert_eq!(s.details.unwrap().name, "test_");
501    }
502
503    #[test]
504    fn failure_with_details3() {
505        let status1 = r#"
506            {
507                "kind": "Status",
508                "apiVersion": "v1",
509                "metadata": {},
510                "status": "Failure",
511                "message": "pods \"foobar-1\" not found",
512                "reason": "NotFound",
513                "details": {
514                    "name": "foobar-1",
515                    "kind": "pods"
516                },
517                "code": 404
518            }"#;
519        let s = serde_json::from_str::<Status>(status1).unwrap();
520        assert!(s.is_not_found());
521        assert_eq!(s.status.unwrap(), StatusSummary::Failure);
522        assert_eq!(s.details.unwrap().name, "foobar-1");
523    }
524
525    #[test]
526    fn expired_with_continue_token() {
527        let status = r#"
528            {
529              "kind": "Status",
530              "apiVersion": "v1",
531              "metadata": {
532                "continue": "<NEW_CONTINUE_TOKEN>"
533              },
534              "status": "Failure",
535              "message": "The provided continue parameter is too old to display a consistent list result.",
536              "reason": "Expired",
537              "code": 410
538            }"#;
539        let s = serde_json::from_str::<Status>(status).unwrap();
540        assert_eq!(s.reason, "Expired");
541        assert_eq!(s.code, 410);
542        assert_eq!(
543            s.metadata.unwrap().continue_.as_deref(),
544            Some("<NEW_CONTINUE_TOKEN>")
545        );
546    }
547}