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}