1use either::{Either, Left, Right};
11use futures::{AsyncBufRead, StreamExt, TryStream, TryStreamExt, future::BoxFuture};
12use http::{self, Request, Response};
13use http_body_util::BodyExt;
14#[cfg(feature = "ws")]
15use hyper_util::rt::TokioIo;
16use jiff::Timestamp;
17use k8s_openapi::apimachinery::pkg::apis::meta::v1 as k8s_meta_v1;
18use kube_core::{discovery::v2::ACCEPT_AGGREGATED_DISCOVERY_V2, response::Status};
19use serde::de::DeserializeOwned;
20use serde_json::{self, Value};
21#[cfg(feature = "ws")]
22use tokio_tungstenite::{WebSocketStream, tungstenite as ws};
23use tokio_util::{
24 codec::{FramedRead, LinesCodec, LinesCodecError},
25 io::StreamReader,
26};
27use tower::{BoxError, Service, ServiceExt as _, buffer::Buffer};
28use tower_http::ServiceExt as _;
29
30pub use self::body::Body;
31use crate::{Config, Error, Result, api::WatchEvent, config::Kubeconfig};
32
33mod auth;
34mod body;
35mod builder;
36pub use kube_core::discovery::v2::{
37 APIGroupDiscovery, APIGroupDiscoveryList, APIResourceDiscovery, APISubresourceDiscovery,
38 APIVersionDiscovery, GroupVersionKind as DiscoveryGroupVersionKind,
39};
40#[cfg_attr(docsrs, doc(cfg(feature = "unstable-client")))]
41#[cfg(feature = "unstable-client")]
42mod client_ext;
43#[cfg_attr(docsrs, doc(cfg(feature = "unstable-client")))]
44#[cfg(feature = "unstable-client")]
45pub use client_ext::scope;
46mod config_ext;
47pub use auth::Error as AuthError;
48pub use config_ext::ConfigExt;
49pub mod middleware;
50pub mod retry;
51
52#[cfg(any(feature = "rustls-tls", feature = "openssl-tls"))]
53mod tls;
54
55#[cfg(feature = "openssl-tls")]
56pub use tls::openssl_tls::Error as OpensslTlsError;
57#[cfg(feature = "rustls-tls")]
58pub use tls::rustls_tls::Error as RustlsTlsError;
59#[cfg(feature = "ws")]
60mod upgrade;
61
62#[cfg(feature = "oauth")]
63#[cfg_attr(docsrs, doc(cfg(feature = "oauth")))]
64pub use auth::OAuthError;
65
66#[cfg(feature = "oidc")]
67#[cfg_attr(docsrs, doc(cfg(feature = "oidc")))]
68pub use auth::oidc_errors;
69
70#[cfg(feature = "ws")]
71pub use upgrade::UpgradeConnectionError;
72
73#[cfg(feature = "kubelet-debug")]
74#[cfg_attr(docsrs, doc(cfg(feature = "kubelet-debug")))]
75mod kubelet_debug;
76
77pub use builder::{ClientBuilder, DynBody};
78
79#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
86#[derive(Clone)]
87pub struct Client {
88 inner: Buffer<Request<Body>, BoxFuture<'static, Result<Response<Body>, BoxError>>>,
91 default_ns: String,
92 valid_until: Option<Timestamp>,
93}
94
95#[cfg(feature = "ws")]
98#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
99pub struct Connection {
100 stream: WebSocketStream<TokioIo<hyper::upgrade::Upgraded>>,
101 protocol: upgrade::StreamProtocol,
102}
103
104#[cfg(feature = "ws")]
105#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
106impl Connection {
107 pub fn supports_stream_close(&self) -> bool {
109 self.protocol.supports_stream_close()
110 }
111
112 pub fn into_stream(self) -> WebSocketStream<TokioIo<hyper::upgrade::Upgraded>> {
114 self.stream
115 }
116}
117
118impl Client {
124 pub fn new<S, B, T>(service: S, default_namespace: T) -> Self
154 where
155 S: Service<Request<Body>, Response = Response<B>> + Send + 'static,
156 S::Future: Send + 'static,
157 S::Error: Into<BoxError>,
158 B: http_body::Body<Data = bytes::Bytes> + Send + 'static,
159 B::Error: Into<BoxError>,
160 T: Into<String>,
161 {
162 let service = service
164 .map_response_body(Body::wrap_body)
165 .map_err(Into::into)
166 .boxed();
167 Self {
168 inner: Buffer::new(service, 1024),
169 default_ns: default_namespace.into(),
170 valid_until: None,
171 }
172 }
173
174 pub fn with_valid_until(self, valid_until: Option<Timestamp>) -> Self {
176 Client { valid_until, ..self }
177 }
178
179 pub fn valid_until(&self) -> &Option<Timestamp> {
181 &self.valid_until
182 }
183
184 pub async fn try_default() -> Result<Self> {
202 Self::try_from(Config::infer().await.map_err(Error::InferConfig)?)
203 }
204
205 pub fn default_namespace(&self) -> &str {
211 &self.default_ns
212 }
213
214 pub async fn send(&self, request: Request<Body>) -> Result<Response<Body>> {
218 let mut svc = self.inner.clone();
219 let res = svc
220 .ready()
221 .await
222 .map_err(Error::Service)?
223 .call(request)
224 .await
225 .map_err(|err| {
226 err.downcast::<Error>()
228 .map(|e| *e)
229 .or_else(|err| err.downcast::<hyper::Error>().map(|err| Error::HyperError(*err)))
231 .unwrap_or_else(Error::Service)
233 })?;
234 Ok(res)
235 }
236
237 #[cfg(feature = "ws")]
239 #[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
240 pub async fn connect(&self, request: Request<Vec<u8>>) -> Result<Connection> {
241 use http::header::HeaderValue;
242 let (mut parts, body) = request.into_parts();
243 parts
244 .headers
245 .insert(http::header::CONNECTION, HeaderValue::from_static("Upgrade"));
246 parts
247 .headers
248 .insert(http::header::UPGRADE, HeaderValue::from_static("websocket"));
249 parts.headers.insert(
250 http::header::SEC_WEBSOCKET_VERSION,
251 HeaderValue::from_static("13"),
252 );
253 let key = tokio_tungstenite::tungstenite::handshake::client::generate_key();
254 parts.headers.insert(
255 http::header::SEC_WEBSOCKET_KEY,
256 key.parse().expect("valid header value"),
257 );
258 upgrade::StreamProtocol::add_to_headers(&mut parts.headers)?;
259
260 let res = self.send(Request::from_parts(parts, Body::from(body))).await?;
261 let res = handle_api_errors(res).await?;
264 let protocol = upgrade::verify_response(&res, &key).map_err(Error::UpgradeConnection)?;
265 match hyper::upgrade::on(res).await {
266 Ok(upgraded) => Ok(Connection {
267 stream: WebSocketStream::from_raw_socket(
268 TokioIo::new(upgraded),
269 ws::protocol::Role::Client,
270 None,
271 )
272 .await,
273 protocol,
274 }),
275
276 Err(e) => Err(Error::UpgradeConnection(
277 UpgradeConnectionError::GetPendingUpgrade(e),
278 )),
279 }
280 }
281
282 pub async fn request<T>(&self, request: Request<Vec<u8>>) -> Result<T>
285 where
286 T: DeserializeOwned,
287 {
288 let text = self.request_text(request).await?;
289
290 serde_json::from_str(&text).map_err(|e| {
291 tracing::warn!("{}, {:?}", text, e);
292 Error::SerdeError(e)
293 })
294 }
295
296 pub async fn request_text(&self, request: Request<Vec<u8>>) -> Result<String> {
299 let res = self.send(request.map(Body::from)).await?;
300 let res = handle_api_errors(res).await?;
301 let body_bytes = res.into_body().collect().await?.to_bytes();
302 let text = String::from_utf8(body_bytes.to_vec()).map_err(Error::FromUtf8)?;
303 Ok(text)
304 }
305
306 pub async fn request_stream(&self, request: Request<Vec<u8>>) -> Result<impl AsyncBufRead + use<>> {
311 let res = self.send(request.map(Body::from)).await?;
312 let res = handle_api_errors(res).await?;
313 let body = res.into_body().into_data_stream().map_err(std::io::Error::other);
316 Ok(body.into_async_read())
317 }
318
319 pub async fn request_status<T>(&self, request: Request<Vec<u8>>) -> Result<Either<T, Status>>
322 where
323 T: DeserializeOwned,
324 {
325 let text = self.request_text(request).await?;
326 let v: Value = serde_json::from_str(&text).map_err(Error::SerdeError)?;
328 if v["kind"] == "Status" {
329 tracing::trace!("Status from {}", text);
330 Ok(Right(serde_json::from_str::<Status>(&text).map_err(|e| {
331 tracing::warn!("{}, {:?}", text, e);
332 Error::SerdeError(e)
333 })?))
334 } else {
335 Ok(Left(serde_json::from_str::<T>(&text).map_err(|e| {
336 tracing::warn!("{}, {:?}", text, e);
337 Error::SerdeError(e)
338 })?))
339 }
340 }
341
342 pub async fn request_events<T>(
344 &self,
345 mut request: Request<Vec<u8>>,
346 ) -> Result<impl TryStream<Item = Result<WatchEvent<T>>> + use<T>>
347 where
348 T: Clone + DeserializeOwned,
349 {
350 request
355 .headers_mut()
356 .entry(http::header::ACCEPT_ENCODING)
357 .or_insert(http::HeaderValue::from_static("identity"));
358 let res = self.send(request.map(Body::from)).await?;
359 let res = handle_api_errors(res).await?;
360 tracing::trace!("headers: {:?}", res.headers());
362
363 let frames = FramedRead::new(
364 StreamReader::new(res.into_body().into_data_stream().map_err(|e| {
365 let chunk_eof = e.to_string().contains("unexpected EOF during chunk");
370 if error_chain_contains_unexpected_eof(&e) || chunk_eof {
371 return std::io::Error::new(std::io::ErrorKind::UnexpectedEof, e);
372 }
373 std::io::Error::other(e)
374 })),
375 LinesCodec::new(),
376 );
377
378 Ok(frames.filter_map(|res| async {
379 match res {
380 Ok(line) => match serde_json::from_str::<WatchEvent<T>>(&line) {
381 Ok(event) => Some(Ok(event)),
382 Err(e) => {
383 if e.is_eof() {
385 return None;
386 }
387
388 if let Ok(status) = serde_json::from_str::<Status>(&line) {
390 return Some(Err(Error::Api(status.boxed())));
391 }
392 Some(Err(Error::SerdeError(e)))
394 }
395 },
396
397 Err(LinesCodecError::Io(e)) => match e.kind() {
398 std::io::ErrorKind::TimedOut => {
400 tracing::warn!("timeout in poll: {}", e); None
402 }
403 std::io::ErrorKind::UnexpectedEof => {
407 tracing::warn!("eof in poll: {:?}", e);
408 None
409 }
410 _ => Some(Err(Error::ReadEvents(e))),
411 },
412
413 Err(LinesCodecError::MaxLineLengthExceeded) => {
416 Some(Err(Error::LinesCodecMaxLineLengthExceeded))
417 }
418 }
419 }))
420 }
421}
422
423impl Client {
429 pub async fn apiserver_version(&self) -> Result<k8s_openapi::apimachinery::pkg::version::Info> {
431 self.request(Request::get("/version").body(vec![]).map_err(Error::HttpError)?)
432 .await
433 }
434
435 pub async fn list_api_groups(&self) -> Result<k8s_meta_v1::APIGroupList> {
437 self.request(Request::get("/apis").body(vec![]).map_err(Error::HttpError)?)
438 .await
439 }
440
441 pub async fn list_api_group_resources(&self, apiversion: &str) -> Result<k8s_meta_v1::APIResourceList> {
460 let url = format!("/apis/{apiversion}");
461 self.request(Request::get(url).body(vec![]).map_err(Error::HttpError)?)
462 .await
463 }
464
465 pub async fn list_core_api_versions(&self) -> Result<k8s_meta_v1::APIVersions> {
467 self.request(Request::get("/api").body(vec![]).map_err(Error::HttpError)?)
468 .await
469 }
470
471 pub async fn list_core_api_resources(&self, version: &str) -> Result<k8s_meta_v1::APIResourceList> {
473 let url = format!("/api/{version}");
474 self.request(Request::get(url).body(vec![]).map_err(Error::HttpError)?)
475 .await
476 }
477}
478
479impl Client {
485 pub async fn list_api_groups_aggregated(&self) -> Result<APIGroupDiscoveryList> {
510 self.request(
511 Request::get("/apis")
512 .header(http::header::ACCEPT, ACCEPT_AGGREGATED_DISCOVERY_V2)
513 .body(vec![])
514 .map_err(Error::HttpError)?,
515 )
516 .await
517 }
518
519 pub async fn list_core_api_versions_aggregated(&self) -> Result<APIGroupDiscoveryList> {
542 self.request(
543 Request::get("/api")
544 .header(http::header::ACCEPT, ACCEPT_AGGREGATED_DISCOVERY_V2)
545 .body(vec![])
546 .map_err(Error::HttpError)?,
547 )
548 .await
549 }
550}
551
552fn error_chain_contains_unexpected_eof(err: &(dyn std::error::Error + 'static)) -> bool {
555 const MAX_SOURCE_DEPTH: usize = 16;
558 let mut current = Some(err);
559 for _ in 0..MAX_SOURCE_DEPTH {
560 let Some(err) = current else {
561 return false;
562 };
563 if let Some(io_err) = err.downcast_ref::<std::io::Error>()
564 && io_err.kind() == std::io::ErrorKind::UnexpectedEof
565 {
566 return true;
567 }
568 current = err.source();
569 }
570 false
571}
572
573async fn handle_api_errors(res: Response<Body>) -> Result<Response<Body>> {
580 let status = res.status();
581 if status.is_client_error() || status.is_server_error() {
582 let body_bytes = res.into_body().collect().await?.to_bytes();
584 let text = String::from_utf8(body_bytes.to_vec()).map_err(Error::FromUtf8)?;
585 if let Ok(status) = serde_json::from_str::<Status>(&text) {
588 tracing::debug!("Unsuccessful: {status:?}");
589 Err(Error::Api(status.boxed()))
590 } else {
591 let text = text.trim();
597 tracing::debug!("Unsuccessful data error parse: {text}");
598 let status = Status::failure(text, "Failed to parse error data").with_code(status.as_u16());
599 tracing::debug!("Unsuccessful: {status:?} (reconstruct)");
600 Err(Error::Api(status.boxed()))
601 }
602 } else {
603 Ok(res)
604 }
605}
606
607impl TryFrom<Config> for Client {
608 type Error = Error;
609
610 fn try_from(config: Config) -> Result<Self> {
614 Ok(ClientBuilder::try_from(config)?.build())
615 }
616}
617
618impl TryFrom<Kubeconfig> for Client {
619 type Error = Error;
620
621 fn try_from(kubeconfig: Kubeconfig) -> Result<Self> {
622 let config = Config::try_from(kubeconfig)?;
623 Ok(ClientBuilder::try_from(config)?.build())
624 }
625}
626
627#[cfg(test)]
628mod tests {
629 use std::{fmt, io::ErrorKind, pin::pin, time::Duration};
630
631 use crate::{
632 Api, Client, Error, Result,
633 api::WatchEvent,
634 client::Body,
635 config::{AuthInfo, Cluster, Context, Kubeconfig, NamedAuthInfo, NamedCluster, NamedContext},
636 };
637
638 use bytes::Bytes;
639 use futures::{Stream, StreamExt};
640 use http::{Request, Response};
641 use http_body::Frame;
642 use http_body_util::StreamBody;
643 use k8s_openapi::api::core::v1::Pod;
644 use kube_core::metadata::PartialObjectMeta;
645 use tower_test::mock;
646
647 const WATCH_POLL_TIMEOUT: Duration = Duration::from_secs(2);
648
649 #[tokio::test]
650 async fn test_default_ns() {
651 let (mock_service, _) = mock::pair::<Request<Body>, Response<Body>>();
652 let client = Client::new(mock_service, "test-namespace");
653 assert_eq!(client.default_namespace(), "test-namespace");
654 }
655
656 #[tokio::test]
657 async fn test_try_from_kubeconfig() {
658 let config = Kubeconfig {
659 current_context: Some("test-context".to_string()),
660 auth_infos: vec![NamedAuthInfo {
661 name: "test-user".to_string(),
662 auth_info: Some(AuthInfo::default()), ..Default::default()
664 }],
665 contexts: vec![NamedContext {
666 name: "test-context".to_string(),
667 context: Some(Context {
668 cluster: "test-cluster".to_string(),
669 user: Some("test-user".to_string()),
670 namespace: Some("test-namespace".to_string()),
671 ..Default::default()
672 }),
673 ..Default::default()
674 }],
675 clusters: vec![NamedCluster {
676 name: "test-cluster".to_string(),
677 cluster: Some(Cluster {
678 server: Some("http://localhost:8080".to_string()),
679 ..Default::default()
680 }),
681 ..Default::default()
682 }],
683 ..Default::default()
684 };
685 let client = Client::try_from(config).expect("Failed to create client from kubeconfig");
686 assert_eq!(client.default_namespace(), "test-namespace");
687 }
688
689 #[tokio::test]
690 async fn test_mock() {
691 let (mock_service, handle) = mock::pair::<Request<Body>, Response<Body>>();
692 let spawned = tokio::spawn(async move {
693 let mut handle = pin!(handle);
695 let (request, send) = handle.next_request().await.expect("service not called");
696 assert_eq!(request.method(), http::Method::GET);
697 assert_eq!(request.uri().to_string(), "/api/v1/namespaces/default/pods/test");
698 let pod: Pod = serde_json::from_value(serde_json::json!({
699 "apiVersion": "v1",
700 "kind": "Pod",
701 "metadata": {
702 "name": "test",
703 "annotations": { "kube-rs": "test" },
704 },
705 "spec": {
706 "containers": [{ "name": "test", "image": "test-image" }],
707 }
708 }))
709 .unwrap();
710 send.send_response(Response::new(Body::from(serde_json::to_vec(&pod).unwrap())));
711 });
712
713 let pods: Api<Pod> = Api::default_namespaced(Client::new(mock_service, "default"));
714 let pod = pods.get("test").await.unwrap();
715 assert_eq!(pod.metadata.annotations.unwrap().get("kube-rs").unwrap(), "test");
716 spawned.await.unwrap();
717 }
718
719 #[tokio::test]
720 async fn test_partial_object_meta_get_uses_metadata_header() {
721 let (mock_service, handle) = mock::pair::<Request<Body>, Response<Body>>();
722 let spawned = tokio::spawn(async move {
723 let mut handle = pin!(handle);
724 let (request, send) = handle.next_request().await.expect("service not called");
725 assert_eq!(
727 request.headers().get(http::header::ACCEPT).unwrap(),
728 "application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1"
729 );
730 let pod_meta: PartialObjectMeta<Pod> =
731 serde_json::from_value(serde_json::json!({
732 "apiVersion": "meta.k8s.io/v1",
733 "kind": "PartialObjectMetadata",
734 "metadata": {
735 "name": "test",
736 },
737 }))
738 .unwrap();
739 send.send_response(Response::new(Body::from(
740 serde_json::to_vec(&pod_meta).unwrap(),
741 )));
742 });
743
744 let pods: Api<PartialObjectMeta<Pod>> =
745 Api::default_namespaced(Client::new(mock_service, "default"));
746 let pod = pods.get("test").await.unwrap();
747 assert_eq!(pod.metadata.name, Some("test".to_string()));
748 spawned.await.unwrap();
749 }
750
751 #[tokio::test]
752 async fn test_partial_object_meta_list_uses_metadata_header() {
753 let (mock_service, handle) = mock::pair::<Request<Body>, Response<Body>>();
754 let spawned = tokio::spawn(async move {
755 let mut handle = pin!(handle);
756 let (request, send) = handle.next_request().await.expect("service not called");
757 assert_eq!(
758 request.headers().get(http::header::ACCEPT).unwrap(),
759 "application/json;as=PartialObjectMetadataList;g=meta.k8s.io;v=v1"
760 );
761 send.send_response(Response::new(Body::from(
762 serde_json::to_vec(&serde_json::json!({
763 "apiVersion": "meta.k8s.io/v1",
764 "kind": "PartialObjectMetadataList",
765 "metadata": { "resourceVersion": "1" },
766 "items": [],
767 }))
768 .unwrap(),
769 )));
770 });
771
772 let pods: Api<PartialObjectMeta<Pod>> =
773 Api::default_namespaced(Client::new(mock_service, "default"));
774 let list = pods.list(&Default::default()).await.unwrap();
775 assert_eq!(list.items.len(), 0);
776 spawned.await.unwrap();
777 }
778
779 #[tokio::test]
780 async fn test_partial_object_meta_patch_uses_metadata_header() {
781 use kube_core::params::{Patch, PatchParams};
782
783 let (mock_service, handle) = mock::pair::<Request<Body>, Response<Body>>();
784 let spawned = tokio::spawn(async move {
785 let mut handle = pin!(handle);
786 let (request, send) = handle.next_request().await.expect("service not called");
787 assert_eq!(request.method(), http::Method::PATCH);
788 assert_eq!(
789 request.headers().get(http::header::ACCEPT).unwrap(),
790 "application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1"
791 );
792 send.send_response(Response::new(Body::from(
793 serde_json::to_vec(&serde_json::json!({
794 "apiVersion": "meta.k8s.io/v1",
795 "kind": "PartialObjectMetadata",
796 "metadata": { "name": "test" },
797 }))
798 .unwrap(),
799 )));
800 });
801
802 let pods: Api<PartialObjectMeta<Pod>> =
803 Api::default_namespaced(Client::new(mock_service, "default"));
804 let patch = Patch::Merge(serde_json::json!({ "metadata": { "labels": { "a": "b" } } }));
805 let pod = pods.patch("test", &PatchParams::default(), &patch).await.unwrap();
806 assert_eq!(pod.metadata.name, Some("test".to_string()));
807 spawned.await.unwrap();
808 }
809
810 #[tokio::test]
811 async fn test_partial_object_meta_watch_uses_metadata_header() {
812 let (mock_service, handle) = mock::pair::<Request<Body>, Response<Body>>();
813 let spawned = tokio::spawn(async move {
814 let mut handle = pin!(handle);
815 let (request, send) = handle.next_request().await.expect("service not called");
816 assert_eq!(
817 request.headers().get(http::header::ACCEPT).unwrap(),
818 "application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1"
819 );
820 send.send_response(Response::new(Body::from(vec![])));
822 });
823
824 let pods: Api<PartialObjectMeta<Pod>> =
825 Api::default_namespaced(Client::new(mock_service, "default"));
826 let _ = pods.watch(&Default::default(), "0").await;
827 spawned.await.unwrap();
828 }
829
830 #[tokio::test]
831 async fn test_watch_error_response_is_surfaced() {
832 let (mock_service, handle) = mock::pair::<Request<Body>, Response<Body>>();
833 let spawned = tokio::spawn(async move {
834 let mut handle = pin!(handle);
835 let (_request, send) = handle.next_request().await.expect("service not called");
836 let body = serde_json::to_vec_pretty(&serde_json::json!({
838 "kind": "Status",
839 "apiVersion": "v1",
840 "metadata": {},
841 "status": "Failure",
842 "message": "pods is forbidden",
843 "reason": "Forbidden",
844 "code": 403,
845 }))
846 .unwrap();
847 send.send_response(
848 Response::builder()
849 .status(http::StatusCode::FORBIDDEN)
850 .body(Body::from(body))
851 .unwrap(),
852 );
853 });
854
855 let pods: Api<Pod> = Api::default_namespaced(Client::new(mock_service, "default"));
856 let Err(err) = pods.watch(&Default::default(), "0").await else {
857 panic!("watch with an error response should fail");
858 };
859 assert!(matches!(&err, Error::Api(s) if s.code == 403), "got {err:?}");
860 spawned.await.unwrap();
861 }
862
863 #[tokio::test]
864 async fn test_non_json_error_response_is_reconstructed() {
865 let (mock_service, handle) = mock::pair::<Request<Body>, Response<Body>>();
866 let spawned = tokio::spawn(async move {
867 let mut handle = pin!(handle);
868 let (_request, send) = handle.next_request().await.expect("service not called");
869 send.send_response(
873 Response::builder()
874 .status(http::StatusCode::NOT_FOUND)
875 .body(Body::from(b"404 page not found\n".to_vec()))
876 .unwrap(),
877 );
878 });
879
880 let pods: Api<Pod> = Api::default_namespaced(Client::new(mock_service, "default"));
881 let Err(err) = pods.get("test").await else {
882 panic!("get with a non-JSON error response should fail");
883 };
884 let Error::Api(status) = &err else {
885 panic!("expected Error::Api, got {err:?}");
886 };
887 assert_eq!(status.code, 404);
888 assert_eq!(status.reason, "Failed to parse error data");
889 assert_eq!(status.message, "404 page not found");
890 spawned.await.unwrap();
891 }
892
893 #[tokio::test]
894 async fn watch_direct_unexpected_eof_delivers_added_then_ends() {
895 let body = body_from_chunks(vec![
898 Ok(pod_event_frame("ADDED", "11")),
899 Err(std::io::Error::new(ErrorKind::UnexpectedEof, "connection closed")),
900 ]);
901 let (spawned, stream) = open_watch(body).await;
902 let mut stream = pin!(stream);
903
904 expect_pod(next_event(&mut stream).await, "ADDED", "11");
905 expect_end(next_event(&mut stream).await);
906 spawned.await.unwrap();
907 }
908
909 #[tokio::test]
910 async fn watch_wrapped_unexpected_eof_delivers_modified_then_ends() {
911 let body = body_from_chunks(vec![
912 Ok(pod_event_frame("MODIFIED", "22")),
913 Err(nested_io(
914 &["outer transport interruption", "inner transport interruption"],
915 std::io::Error::new(ErrorKind::UnexpectedEof, "connection closed"),
916 )),
917 ]);
918 let (spawned, stream) = open_watch(body).await;
919 let mut stream = pin!(stream);
920
921 expect_pod(next_event(&mut stream).await, "MODIFIED", "22");
922 expect_end(next_event(&mut stream).await);
923 spawned.await.unwrap();
924 }
925
926 #[tokio::test]
927 async fn watch_unexpected_eof_before_events_ends() {
928 let body = body_from_chunks(vec![Err(nested_io(
929 &["outer transport interruption", "inner transport interruption"],
930 std::io::Error::new(ErrorKind::UnexpectedEof, "connection closed"),
931 ))]);
932 let (spawned, stream) = open_watch(body).await;
933 let mut stream = pin!(stream);
934
935 expect_end(next_event(&mut stream).await);
936 spawned.await.unwrap();
937 }
938
939 #[tokio::test]
940 async fn watch_unexpected_eof_after_truncated_line_keeps_resource_version() {
941 let mut payload = pod_event_json("ADDED", "33");
942 payload.push('\n');
943 payload.push_str(r#"{"type":"MODIFIED","object":{"apiVersion":"v1""#);
944 let body = body_from_chunks(vec![
945 Ok(Bytes::from(payload)),
946 Err(std::io::Error::new(ErrorKind::UnexpectedEof, "connection closed")),
947 ]);
948 let (spawned, stream) = open_watch(body).await;
949 let mut stream = pin!(stream);
950
951 expect_pod(next_event(&mut stream).await, "ADDED", "33");
952 expect_end(next_event(&mut stream).await);
953 spawned.await.unwrap();
954 }
955
956 #[tokio::test]
957 async fn watch_clean_end_after_truncated_line_keeps_resource_version() {
958 let mut payload = pod_event_json("MODIFIED", "34");
959 payload.push('\n');
960 payload.push_str(r#"{"type":"ADDED","object":{"apiVersion":"v1""#);
961 let body = body_from_chunks::<std::io::Error>(vec![Ok(Bytes::from(payload))]);
962 let (spawned, stream) = open_watch(body).await;
963 let mut stream = pin!(stream);
964
965 expect_pod(next_event(&mut stream).await, "MODIFIED", "34");
966 expect_end(next_event(&mut stream).await);
967 spawned.await.unwrap();
968 }
969
970 #[tokio::test]
971 async fn watch_clean_termination_delivers_every_event() {
972 let payload = format!("{}{}", pod_event_frame_str("ADDED", "1"), pod_event_frame_str("MODIFIED", "2"));
973 let body = body_from_chunks::<std::io::Error>(vec![Ok(Bytes::from(payload))]);
974 let (spawned, stream) = open_watch(body).await;
975 let mut stream = pin!(stream);
976
977 expect_pod(next_event(&mut stream).await, "ADDED", "1");
978 expect_pod(next_event(&mut stream).await, "MODIFIED", "2");
979 expect_end(next_event(&mut stream).await);
980 spawned.await.unwrap();
981 }
982
983 #[tokio::test]
984 async fn watch_chunk_decoder_eof_text_ends_stream() {
985 let body = body_from_chunks(vec![
986 Ok(pod_event_frame("ADDED", "44")),
987 Err(ChunkDecoderEof),
988 ]);
989 let (spawned, stream) = open_watch(body).await;
990 let mut stream = pin!(stream);
991
992 expect_pod(next_event(&mut stream).await, "ADDED", "44");
993 expect_end(next_event(&mut stream).await);
994 spawned.await.unwrap();
995 }
996
997 #[tokio::test]
998 async fn watch_malformed_json_is_serde_error() {
999 let body = body_from_chunks::<std::io::Error>(vec![Ok(Bytes::from(
1000 "{\"type\":\"ADDED\",\"object\":}\n",
1001 ))]);
1002 let (spawned, stream) = open_watch(body).await;
1003 let mut stream = pin!(stream);
1004
1005 match next_event(&mut stream).await {
1006 Some(Err(Error::SerdeError(err))) => assert!(!err.is_eof(), "{err}"),
1007 other => panic!("expected a non-eof serde error, got {other:?}"),
1008 }
1009 spawned.await.unwrap();
1010 }
1011
1012 #[tokio::test]
1013 async fn watch_nested_non_eof_io_error_is_read_events() {
1014 let body = body_from_chunks(vec![
1015 Ok(pod_event_frame("ADDED", "55")),
1016 Err(nested_io(
1017 &["outer transport interruption", "inner transport interruption"],
1018 std::io::Error::new(ErrorKind::ConnectionReset, "socket reset"),
1019 )),
1020 ]);
1021 let (spawned, stream) = open_watch(body).await;
1022 let mut stream = pin!(stream);
1023
1024 expect_pod(next_event(&mut stream).await, "ADDED", "55");
1025 let io_err = expect_read_events(next_event(&mut stream).await);
1026 assert_eq!(io_err.kind(), ErrorKind::Other);
1027 let retained = io_err.get_ref().expect("original body error is the source");
1028 let body_err = retained.downcast_ref::<Error>().expect("source is the body error");
1029 assert!(
1030 chain_has_io_kind(body_err, ErrorKind::ConnectionReset),
1031 "original source chain dropped ConnectionReset: {body_err:?}"
1032 );
1033 assert!(
1034 chain_has_message(body_err, "outer transport interruption"),
1035 "{body_err:?}"
1036 );
1037 assert!(
1038 chain_has_message(body_err, "inner transport interruption"),
1039 "{body_err:?}"
1040 );
1041 spawned.await.unwrap();
1042 }
1043
1044 #[tokio::test]
1045 async fn watch_close_notify_message_with_other_io_kind_is_read_events() {
1046 let body = body_from_chunks(vec![
1047 Ok(pod_event_frame("MODIFIED", "66")),
1048 Err(std::io::Error::new(
1049 ErrorKind::InvalidData,
1050 "peer closed connection without sending TLS close_notify",
1051 )),
1052 ]);
1053 let (spawned, stream) = open_watch(body).await;
1054 let mut stream = pin!(stream);
1055
1056 expect_pod(next_event(&mut stream).await, "MODIFIED", "66");
1057 let io_err = expect_read_events(next_event(&mut stream).await);
1058 assert_eq!(io_err.kind(), ErrorKind::Other);
1059 let retained = io_err.get_ref().expect("original body error is the source");
1060 let body_err = retained.downcast_ref::<Error>().expect("source is the body error");
1061 assert!(
1062 chain_has_message(body_err, "close_notify"),
1063 "fixture must mention close_notify, got {body_err}"
1064 );
1065 assert!(
1066 chain_has_io_kind(body_err, ErrorKind::InvalidData),
1067 "original InvalidData kind dropped: {body_err:?}"
1068 );
1069 assert!(
1070 !chain_has_io_kind(body_err, ErrorKind::UnexpectedEof),
1071 "a close_notify message with a different kind was treated as eof: {body_err:?}"
1072 );
1073 spawned.await.unwrap();
1074 }
1075
1076 fn pod_event_json(action: &str, resource_version: &str) -> String {
1077 serde_json::json!({
1078 "type": action,
1079 "object": {
1080 "apiVersion": "v1",
1081 "kind": "Pod",
1082 "metadata": {
1083 "name": "test",
1084 "namespace": "default",
1085 "resourceVersion": resource_version,
1086 },
1087 "spec": {
1088 "containers": [{ "name": "test", "image": "test-image" }],
1089 }
1090 }
1091 })
1092 .to_string()
1093 }
1094
1095 fn pod_event_frame_str(action: &str, resource_version: &str) -> String {
1096 format!("{}\n", pod_event_json(action, resource_version))
1097 }
1098
1099 fn pod_event_frame(action: &str, resource_version: &str) -> Bytes {
1100 Bytes::from(pod_event_frame_str(action, resource_version))
1101 }
1102
1103 fn body_from_chunks<E>(chunks: Vec<Result<Bytes, E>>) -> Body
1104 where
1105 E: std::error::Error + Send + Sync + 'static,
1106 {
1107 let frames = chunks.into_iter().map(|chunk| chunk.map(Frame::data));
1108 Body::wrap_body(StreamBody::new(futures::stream::iter(frames)))
1109 }
1110
1111 async fn open_watch(body: Body) -> (tokio::task::JoinHandle<()>, impl Stream<Item = Result<WatchEvent<Pod>>>) {
1113 let (mock_service, handle) = mock::pair::<Request<Body>, Response<Body>>();
1114 let spawned = tokio::spawn(async move {
1115 let mut handle = pin!(handle);
1116 let (request, send) = handle.next_request().await.expect("service not called");
1117 assert!(
1118 request.uri().query().unwrap_or_default().contains("watch=true"),
1119 "expected the client watch path, got {}",
1120 request.uri()
1121 );
1122 send.send_response(Response::new(body));
1123 });
1124 let pods: Api<Pod> = Api::default_namespaced(Client::new(mock_service, "default"));
1125 let stream = pods.watch(&Default::default(), "0").await.expect("watch response");
1126 (spawned, stream)
1127 }
1128
1129 async fn next_event<S>(stream: &mut S) -> Option<Result<WatchEvent<Pod>>>
1130 where
1131 S: Stream<Item = Result<WatchEvent<Pod>>> + Unpin,
1132 {
1133 tokio::time::timeout(WATCH_POLL_TIMEOUT, stream.next())
1134 .await
1135 .expect("watch stream stalled")
1136 }
1137
1138 fn expect_pod(item: Option<Result<WatchEvent<Pod>>>, action: &str, resource_version: &str) {
1139 let pod = match (action, item) {
1140 ("ADDED", Some(Ok(WatchEvent::Added(pod)))) => pod,
1141 ("MODIFIED", Some(Ok(WatchEvent::Modified(pod)))) => pod,
1142 (_, other) => panic!("expected {action} pod, got {other:?}"),
1143 };
1144 assert_eq!(pod.metadata.name.as_deref(), Some("test"));
1145 assert_eq!(
1146 pod.metadata.resource_version.as_deref(),
1147 Some(resource_version),
1148 "completed event must keep its resource version"
1149 );
1150 }
1151
1152 fn expect_end(item: Option<Result<WatchEvent<Pod>>>) {
1153 assert!(item.is_none(), "expected the watch stream to end, got {item:?}");
1154 }
1155
1156 fn expect_read_events(item: Option<Result<WatchEvent<Pod>>>) -> std::io::Error {
1157 match item {
1158 Some(Err(Error::ReadEvents(err))) => err,
1159 other => panic!("expected ReadEvents, got {other:?}"),
1160 }
1161 }
1162
1163 fn nested_io(messages: &[&'static str], io_err: std::io::Error) -> SourceChain {
1164 let (outer, rest) = messages.split_first().expect("at least one wrapper");
1165 let mut source: Box<dyn std::error::Error + Send + Sync> = Box::new(io_err);
1166 for message in rest.iter().rev() {
1167 source = Box::new(SourceChain {
1168 message,
1169 source: Some(source),
1170 });
1171 }
1172 SourceChain {
1173 message: outer,
1174 source: Some(source),
1175 }
1176 }
1177
1178 fn chain_has_io_kind(err: &(dyn std::error::Error + 'static), kind: ErrorKind) -> bool {
1179 let mut current = Some(err);
1180 for _ in 0..16 {
1181 let Some(err) = current else {
1182 return false;
1183 };
1184 if let Some(io_err) = err.downcast_ref::<std::io::Error>()
1185 && io_err.kind() == kind
1186 {
1187 return true;
1188 }
1189 current = err.source();
1190 }
1191 false
1192 }
1193
1194 fn chain_has_message(err: &(dyn std::error::Error + 'static), message: &str) -> bool {
1195 let mut current = Some(err);
1196 for _ in 0..16 {
1197 let Some(err) = current else {
1198 return false;
1199 };
1200 if err.to_string().contains(message) {
1201 return true;
1202 }
1203 current = err.source();
1204 }
1205 false
1206 }
1207
1208 struct SourceChain {
1210 message: &'static str,
1211 source: Option<Box<dyn std::error::Error + Send + Sync>>,
1212 }
1213
1214 impl fmt::Debug for SourceChain {
1215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1216 f.debug_struct("SourceChain")
1217 .field("message", &self.message)
1218 .field("source", &self.source.as_ref().map(ToString::to_string))
1219 .finish()
1220 }
1221 }
1222
1223 impl fmt::Display for SourceChain {
1224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1225 f.write_str(self.message)
1226 }
1227 }
1228
1229 impl std::error::Error for SourceChain {
1230 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1231 self.source.as_ref().map(|err| err.as_ref() as _)
1232 }
1233 }
1234
1235 #[derive(Debug)]
1238 struct ChunkDecoderEof;
1239
1240 impl fmt::Display for ChunkDecoderEof {
1241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1242 f.write_str("unexpected EOF during chunk size line")
1243 }
1244 }
1245
1246 impl std::error::Error for ChunkDecoderEof {}
1247
1248 #[cfg(feature = "ws")]
1249 #[tokio::test]
1250 async fn connect_returns_api_error_for_forbidden_status() {
1251 let (mock_service, handle) = mock::pair::<Request<Body>, Response<Body>>();
1252 let spawned = tokio::spawn(async move {
1253 let mut handle = pin!(handle);
1254 let (_request, send) = handle.next_request().await.expect("service not called");
1255 let body = serde_json::json!({
1256 "status": "Failure",
1257 "message": "pods \"my-pod\" is forbidden: User \"system:serviceaccount:default:my-sa\" cannot create resource \"pods/exec\"",
1258 "reason": "Forbidden",
1259 "code": 403
1260 });
1261 send.send_response(
1262 Response::builder()
1263 .status(403)
1264 .body(Body::from(serde_json::to_vec(&body).unwrap()))
1265 .unwrap(),
1266 );
1267 });
1268
1269 let client = Client::new(mock_service, "default");
1270 let request = Request::builder()
1271 .uri("/api/v1/namespaces/default/pods/my-pod/exec")
1272 .body(Vec::new())
1273 .unwrap();
1274 let err = match client.connect(request).await {
1275 Err(err) => err,
1276 Ok(_) => panic!("403 is an API error"),
1277 };
1278 match err {
1279 Error::Api(status) => {
1280 assert_eq!(status.code, 403);
1281 assert!(status.message.contains("forbidden"), "{}", status.message);
1282 }
1283 other => panic!("expected Api error, got {other}"),
1284 }
1285 spawned.await.unwrap();
1286 }
1287}