1#![cfg_attr(docsrs, feature(doc_cfg))]
64#![allow(clippy::unnecessary_lazy_evaluations)]
66
67macro_rules! cfg_client {
68 ($($item:item)*) => {
69 $(
70 #[cfg_attr(docsrs, doc(cfg(feature = "client")))]
71 #[cfg(feature = "client")]
72 $item
73 )*
74 }
75}
76macro_rules! cfg_config {
77 ($($item:item)*) => {
78 $(
79 #[cfg_attr(docsrs, doc(cfg(feature = "config")))]
80 #[cfg(feature = "config")]
81 $item
82 )*
83 }
84}
85
86macro_rules! cfg_error {
87 ($($item:item)*) => {
88 $(
89 #[cfg_attr(docsrs, doc(cfg(any(feature = "config", feature = "client"))))]
90 #[cfg(any(feature = "config", feature = "client"))]
91 $item
92 )*
93 }
94}
95
96cfg_client! {
97 pub mod api;
98 pub mod discovery;
99 pub mod client;
100
101 #[doc(inline)]
102 pub use api::Api;
103 #[doc(inline)]
104 pub use client::Client;
105 #[doc(inline)]
106 pub use discovery::Discovery;
107}
108
109cfg_config! {
110 pub mod config;
111 #[doc(inline)]
112 pub use config::Config;
113}
114
115cfg_error! {
116 pub mod error;
117 #[doc(inline)] pub use error::Error;
118 pub type Result<T, E = Error> = std::result::Result<T, E>;
120}
121
122pub use crate::core::{CustomResourceExt, Resource, ResourceExt};
123pub use kube_core as core;
125
126#[cfg(all(feature = "client", feature = "config"))]
129#[cfg(test)]
130#[allow(unused_imports)] mod test {
132 use crate::{
133 Api, Client, Config, ResourceExt,
134 api::{AttachParams, AttachedProcess},
135 client::ConfigExt,
136 };
137 use futures::{AsyncBufRead, AsyncBufReadExt, StreamExt, TryStreamExt};
138 use k8s_openapi::api::core::v1::{EphemeralContainer, Pod, PodSpec};
139 use kube_core::{
140 params::{DeleteParams, Patch, PatchParams, PostParams, WatchParams},
141 response::StatusSummary,
142 };
143 use serde_json::json;
144 use tower::ServiceBuilder;
145
146 #[allow(dead_code)]
148 #[ignore = "needs cluster (lists pods)"]
150 #[cfg(feature = "rustls-tls")]
151 async fn custom_client_rustls_configuration() -> Result<(), Box<dyn std::error::Error>> {
152 use hyper_util::rt::TokioExecutor;
153
154 let config = Config::infer().await?;
155 let https = config.rustls_https_connector()?;
156 let service = ServiceBuilder::new()
157 .layer(config.base_uri_layer())
158 .service(hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(https));
159 let client = Client::new(service, config.default_namespace);
160 let pods: Api<Pod> = Api::default_namespaced(client);
161 pods.list(&Default::default()).await?;
162 Ok(())
163 }
164
165 #[tokio::test]
166 #[ignore = "needs cluster (lists pods)"]
167 #[cfg(feature = "openssl-tls")]
168 async fn custom_client_openssl_tls_configuration() -> Result<(), Box<dyn std::error::Error>> {
169 use hyper_util::rt::TokioExecutor;
170
171 let config = Config::infer().await?;
172 let https = config.openssl_https_connector()?;
173 let service = ServiceBuilder::new()
174 .layer(config.base_uri_layer())
175 .service(hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(https));
176 let client = Client::new(service, config.default_namespace);
177 let pods: Api<Pod> = Api::default_namespaced(client);
178 pods.list(&Default::default()).await?;
179 Ok(())
180 }
181
182 #[tokio::test]
183 #[ignore = "needs cluster (lists api resources)"]
184 #[cfg(feature = "client")]
185 async fn group_discovery_oneshot() -> Result<(), Box<dyn std::error::Error>> {
186 use crate::{core::DynamicObject, discovery};
187 let client = Client::try_default().await?;
188 let apigroup = discovery::group(&client, "apiregistration.k8s.io").await?;
189 let (ar, _caps) = apigroup.recommended_kind("APIService").unwrap();
190 let api: Api<DynamicObject> = Api::all_with(client.clone(), &ar);
191 api.list(&Default::default()).await?;
192
193 Ok(())
194 }
195
196 #[tokio::test]
197 #[ignore = "needs cluster (uses aggregated discovery, requires k8s 1.26+)"]
198 #[cfg(feature = "client")]
199 async fn aggregated_discovery_apis() -> Result<(), Box<dyn std::error::Error>> {
200 let client = Client::try_default().await?;
201
202 let apis_discovery = client.list_api_groups_aggregated().await?;
204 assert!(!apis_discovery.items.is_empty(), "should have API groups");
205
206 let apps_group = apis_discovery
208 .items
209 .iter()
210 .find(|g| g.metadata.as_ref().and_then(|m| m.name.as_ref()) == Some(&"apps".to_string()));
211 assert!(apps_group.is_some(), "should have apps group");
212
213 let apps = apps_group.unwrap();
214 assert!(!apps.versions.is_empty(), "apps should have versions");
215
216 let v1 = apps.versions.iter().find(|v| v.version == Some("v1".to_string()));
218 assert!(v1.is_some(), "apps should have v1");
219
220 let deployments = v1
221 .unwrap()
222 .resources
223 .iter()
224 .find(|r| r.resource == Some("deployments".to_string()));
225 assert!(deployments.is_some(), "apps/v1 should have deployments");
226
227 Ok(())
228 }
229
230 #[tokio::test]
231 #[ignore = "needs cluster (uses aggregated discovery, requires k8s 1.26+)"]
232 #[cfg(feature = "client")]
233 async fn aggregated_discovery_core() -> Result<(), Box<dyn std::error::Error>> {
234 let client = Client::try_default().await?;
235
236 let core_discovery = client.list_core_api_versions_aggregated().await?;
238 assert!(!core_discovery.items.is_empty(), "should have core group");
239
240 let core = &core_discovery.items[0];
241 let v1 = core.versions.iter().find(|v| v.version == Some("v1".to_string()));
242 assert!(v1.is_some(), "core should have v1");
243
244 let pods = v1
246 .unwrap()
247 .resources
248 .iter()
249 .find(|r| r.resource == Some("pods".to_string()));
250 assert!(pods.is_some(), "core/v1 should have pods");
251
252 let pods_resource = pods.unwrap();
253 assert_eq!(pods_resource.scope, Some("Namespaced".to_string()));
254 assert!(pods_resource.verbs.contains(&"list".to_string()));
255
256 Ok(())
257 }
258
259 #[tokio::test]
260 #[ignore = "needs cluster (uses aggregated discovery, requires k8s 1.26+)"]
261 #[cfg(feature = "client")]
262 async fn discovery_run_aggregated() -> Result<(), Box<dyn std::error::Error>> {
263 use crate::discovery::{Discovery, verbs};
264
265 let client = Client::try_default().await?;
266
267 let discovery = Discovery::new(client.clone()).run_aggregated().await?;
269
270 assert!(discovery.groups().count() > 0, "should have discovered groups");
272
273 assert!(discovery.has_group(""), "should have core group");
275
276 assert!(discovery.has_group("apps"), "should have apps group");
278
279 let apps = discovery.get("apps").expect("apps group");
281 let (ar, caps) = apps.recommended_kind("Deployment").expect("Deployment kind");
282 assert_eq!(ar.kind, "Deployment");
283 assert!(caps.supports_operation(verbs::LIST));
284
285 let core = discovery.get("").expect("core group");
287 let (ar, caps) = core.recommended_kind("Pod").expect("Pod kind");
288 assert_eq!(ar.kind, "Pod");
289 assert!(caps.supports_operation(verbs::GET));
290
291 Ok(())
292 }
293
294 #[tokio::test]
295 #[ignore = "needs cluster (will create and edit a pod)"]
296 async fn pod_can_use_core_apis() -> Result<(), Box<dyn std::error::Error>> {
297 use kube::api::{DeleteParams, ListParams, Patch, PatchParams, PostParams, WatchEvent};
298
299 let client = Client::try_default().await?;
300 let pods: Api<Pod> = Api::default_namespaced(client);
301
302 let p: Pod = serde_json::from_value(json!({
304 "apiVersion": "v1",
305 "kind": "Pod",
306 "metadata": {
307 "name": "busybox-kube1",
308 "labels": { "app": "kube-rs-test" },
309 },
310 "spec": {
311 "terminationGracePeriodSeconds": 1,
312 "restartPolicy": "Never",
313 "containers": [{
314 "name": "busybox",
315 "image": "busybox:stable",
316 "command": ["sh", "-c", "sleep 30"],
317 }],
318 }
319 }))?;
320
321 let pp = PostParams::default();
322 match pods.create(&pp, &p).await {
323 Ok(o) => assert_eq!(p.name_unchecked(), o.name_unchecked()),
324 Err(crate::Error::Api(ae)) => assert_eq!(ae.code, 409), Err(e) => return Err(e.into()), }
327
328 let wp = WatchParams::default()
331 .fields(&format!("metadata.name={}", "busybox-kube1"))
332 .timeout(15);
333 let mut stream = pods.watch(&wp, "0").await?.boxed();
334 while let Some(ev) = stream.try_next().await? {
335 let _ = format!("we: {ev:?}");
337 match ev {
338 WatchEvent::Modified(o) => {
339 let s = o.status.as_ref().expect("status exists on pod");
340 let phase = s.phase.clone().unwrap_or_default();
341 if phase == "Running" {
342 break;
343 }
344 }
345 WatchEvent::Error(e) => panic!("watch error: {e}"),
346 _ => {}
347 }
348 }
349
350 let mut pod = pods.get("busybox-kube1").await?;
352 assert_eq!(p.spec.as_ref().unwrap().containers[0].name, "busybox");
353
354 {
357 assert!(pod.resource_version().is_some());
358 pod.spec.as_mut().unwrap().active_deadline_seconds = Some(5);
359
360 let pp = PostParams::default();
361 let patched_pod = pods.replace("busybox-kube1", &pp, &pod).await?;
362 assert_eq!(patched_pod.spec.unwrap().active_deadline_seconds, Some(5));
363 }
364
365 let dp = DeleteParams::default();
367 pods.delete("busybox-kube1", &dp).await?.map_left(|pdel| {
368 assert_eq!(pdel.name_unchecked(), "busybox-kube1");
369 });
370
371 Ok(())
372 }
373
374 #[tokio::test]
375 #[ignore = "needs cluster (will create and attach to a pod)"]
376 #[cfg(feature = "ws")]
377 async fn pod_can_exec_and_write_to_stdin() -> Result<(), Box<dyn std::error::Error>> {
378 use crate::api::{DeleteParams, ListParams, Patch, PatchParams, WatchEvent};
379 use tokio::io::AsyncWriteExt;
380
381 let client = Client::try_default().await?;
382 let pods: Api<Pod> = Api::default_namespaced(client);
383
384 let p: Pod = serde_json::from_value(json!({
386 "apiVersion": "v1",
387 "kind": "Pod",
388 "metadata": {
389 "name": "busybox-kube5",
390 "labels": { "app": "kube-rs-test" },
391 },
392 "spec": {
393 "terminationGracePeriodSeconds": 1,
394 "restartPolicy": "Never",
395 "containers": [{
396 "name": "busybox",
397 "image": "busybox:stable",
398 "command": ["sh", "-c", "sleep 30"],
399 }],
400 }
401 }))?;
402
403 match pods.create(&Default::default(), &p).await {
404 Ok(o) => assert_eq!(p.name_unchecked(), o.name_unchecked()),
405 Err(crate::Error::Api(ae)) => assert_eq!(ae.code, 409), Err(e) => return Err(e.into()), }
408
409 let wp = WatchParams::default()
412 .fields(&format!("metadata.name={}", "busybox-kube5"))
413 .timeout(15);
414 let mut stream = pods.watch(&wp, "0").await?.boxed();
415 while let Some(ev) = stream.try_next().await? {
416 match ev {
417 WatchEvent::Modified(o) => {
418 let s = o.status.as_ref().expect("status exists on pod");
419 let phase = s.phase.clone().unwrap_or_default();
420 if phase != "Running" {
421 continue;
422 }
423 let Some(statuses) = s.container_statuses.as_ref() else {
424 continue;
425 };
426 if statuses.iter().all(|s| s.ready) {
427 break;
428 }
429 }
430 WatchEvent::Error(e) => panic!("watch error: {e}"),
431 _ => {}
432 }
433 }
434
435 {
437 let mut attached = pods
438 .exec(
439 "busybox-kube5",
440 vec!["sh", "-c", "for i in $(seq 1 3); do echo $i; done"],
441 &AttachParams::default().stderr(false),
442 )
443 .await?;
444 let stdout = tokio_util::io::ReaderStream::new(attached.stdout().unwrap());
445 let out = stdout
446 .filter_map(|r| async { r.ok().and_then(|v| String::from_utf8(v.to_vec()).ok()) })
447 .collect::<Vec<_>>()
448 .await
449 .join("");
450 attached.join().await.unwrap();
451 assert_eq!(out.lines().count(), 3);
452 assert_eq!(out, "1\n2\n3\n");
453 }
454
455 {
457 let name = "busybox-kube5";
458 let command = vec!["sh", "-c", "sleep 2; echo test string 2"];
459 let ap = AttachParams::default().stdin(true).stderr(false);
460
461 let mut req = pods.request.exec(name, command.clone(), &ap)?;
463 req.extensions_mut().insert("exec");
464 let stream = pods.client.connect(req).await?;
465
466 if stream.supports_stream_close() {
469 let mut attached = pods.exec(name, command, &ap).await?;
470 let mut stdin_writer = attached.stdin().unwrap();
471 let mut stdout_stream = tokio_util::io::ReaderStream::new(attached.stdout().unwrap());
472
473 stdin_writer.write_all(b"this will be ignored\n").await?;
474 _ = stdin_writer.shutdown().await;
475
476 let status = attached.take_status().unwrap();
478 if let Some(status) = status.await {
479 assert_eq!(status.reason, None);
480 assert_eq!(status.status, Some("Success".to_owned()));
481 }
482
483 let next_stdout = stdout_stream.next();
484 let stdout = String::from_utf8(next_stdout.await.unwrap().unwrap().to_vec()).unwrap();
485 assert_eq!(stdout, "test string 2\n");
486 }
487 }
488
489 {
491 let mut attached = pods
492 .exec(
493 "busybox-kube5",
494 vec!["sh"],
495 &AttachParams::default().stdin(true).stderr(false),
496 )
497 .await?;
498 let mut stdin_writer = attached.stdin().unwrap();
499 let mut stdout_stream = tokio_util::io::ReaderStream::new(attached.stdout().unwrap());
500 let next_stdout = stdout_stream.next();
501 stdin_writer.write_all(b"echo test string 1\n").await?;
502 let stdout = String::from_utf8(next_stdout.await.unwrap().unwrap().to_vec()).unwrap();
503 println!("{stdout}");
504 assert_eq!(stdout, "test string 1\n");
505
506 stdin_writer.write_all(b"exit 1\n").await?;
509 let status = attached.take_status().unwrap();
510 if let Some(status) = status.await {
511 println!("{status:?}");
512 assert_eq!(status.status, Some("Failure".to_owned()));
513 assert_eq!(status.reason, Some("NonZeroExitCode".to_owned()));
514 }
515 }
516
517 let dp = DeleteParams::default();
519 pods.delete("busybox-kube5", &dp).await?.map_left(|pdel| {
520 assert_eq!(pdel.name_unchecked(), "busybox-kube5");
521 });
522
523 Ok(())
524 }
525
526 #[tokio::test]
527 #[ignore = "needs cluster (will create and tail logs from a pod)"]
528 async fn can_get_pod_logs_and_evict() -> Result<(), Box<dyn std::error::Error>> {
529 use crate::{
530 api::{DeleteParams, EvictParams, ListParams, Patch, PatchParams, WatchEvent},
531 core::subresource::LogParams,
532 };
533
534 let client = Client::try_default().await?;
535 let pods: Api<Pod> = Api::default_namespaced(client);
536
537 let p: Pod = serde_json::from_value(json!({
539 "apiVersion": "v1",
540 "kind": "Pod",
541 "metadata": {
542 "name": "busybox-kube3",
543 "labels": { "app": "kube-rs-test" },
544 },
545 "spec": {
546 "terminationGracePeriodSeconds": 1,
547 "restartPolicy": "Never",
548 "containers": [{
549 "name": "busybox",
550 "image": "busybox:stable",
551 "command": ["sh", "-c", "for i in $(seq 1 5); do echo kube $i; sleep 0.1; done"],
552 }],
553 }
554 }))?;
555
556 match pods.create(&Default::default(), &p).await {
557 Ok(o) => assert_eq!(p.name_unchecked(), o.name_unchecked()),
558 Err(crate::Error::Api(ae)) => assert_eq!(ae.code, 409), Err(e) => return Err(e.into()), }
561
562 let wp = WatchParams::default()
565 .fields(&format!("metadata.name={}", "busybox-kube3"))
566 .timeout(15);
567 let mut stream = pods.watch(&wp, "0").await?.boxed();
568 while let Some(ev) = stream.try_next().await? {
569 match ev {
570 WatchEvent::Modified(o) => {
571 let s = o.status.as_ref().expect("status exists on pod");
572 let phase = s.phase.clone().unwrap_or_default();
573 if phase == "Running" {
574 break;
575 }
576 }
577 WatchEvent::Error(e) => panic!("watch error: {e}"),
578 _ => {}
579 }
580 }
581
582 let lp = LogParams {
584 follow: true,
585 ..LogParams::default()
586 };
587 let mut logs_stream = pods.log_stream("busybox-kube3", &lp).await?.lines();
588
589 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
591
592 let all_logs = pods.logs("busybox-kube3", &Default::default()).await?;
593 assert_eq!(all_logs, "kube 1\nkube 2\nkube 3\nkube 4\nkube 5\n");
594
595 let mut output = vec![];
597 while let Some(line) = logs_stream.try_next().await? {
598 output.push(line);
599 }
600 assert_eq!(output, vec!["kube 1", "kube 2", "kube 3", "kube 4", "kube 5"]);
601
602 let ep = EvictParams::default();
604 let eres = pods.evict("busybox-kube3", &ep).await?;
605 assert_eq!(eres.code, 201); assert!(eres.is_success());
607
608 Ok(())
609 }
610
611 #[tokio::test]
612 #[ignore = "requires a cluster"]
613 async fn can_operate_on_pod_metadata() -> Result<(), Box<dyn std::error::Error>> {
614 use crate::{
615 api::{DeleteParams, EvictParams, ListParams, Patch, PatchParams, WatchEvent},
616 core::subresource::LogParams,
617 };
618 use kube_core::{ObjectList, ObjectMeta, PartialObjectMeta, PartialObjectMetaExt};
619
620 let client = Client::try_default().await?;
621 let pods: Api<Pod> = Api::default_namespaced(client);
622
623 let p: Pod = serde_json::from_value(json!({
625 "apiVersion": "v1",
626 "kind": "Pod",
627 "metadata": {
628 "name": "busybox-kube-meta",
629 "labels": { "app": "kube-rs-test" },
630 },
631 "spec": {
632 "terminationGracePeriodSeconds": 1,
633 "restartPolicy": "Never",
634 "containers": [{
635 "name": "busybox",
636 "image": "busybox:stable",
637 "command": ["sh", "-c", "sleep 30s"],
638 }],
639 }
640 }))?;
641
642 match pods.create(&Default::default(), &p).await {
643 Ok(o) => assert_eq!(p.name_unchecked(), o.name_unchecked()),
644 Err(crate::Error::Api(ae)) => assert_eq!(ae.code, 409), Err(e) => return Err(e.into()), }
647
648 let pod_metadata = pods.get_metadata("busybox-kube-meta").await?;
651 assert_eq!("busybox-kube-meta", pod_metadata.name_any());
652 assert_eq!(
653 Some((&"app".to_string(), &"kube-rs-test".to_string())),
654 pod_metadata.labels().get_key_value("app")
655 );
656
657 let p_list = pods.list_metadata(&ListParams::default()).await?;
659
660 let pod_metadata = p_list
663 .items
664 .into_iter()
665 .find(|p| p.name_any() == "busybox-kube-meta")
666 .unwrap();
667 assert_eq!(
668 pod_metadata.labels().get("app"),
669 Some(&"kube-rs-test".to_string())
670 );
671
672 let patch = ObjectMeta {
674 annotations: Some([("test".to_string(), "123".to_string())].into()),
675 ..Default::default()
676 }
677 .into_request_partial::<Pod>();
678
679 let patchparams = PatchParams::default();
680 let p_patched = pods
681 .patch_metadata("busybox-kube-meta", &patchparams, &Patch::Merge(&patch))
682 .await?;
683 assert_eq!(p_patched.annotations().get("test"), Some(&"123".to_string()));
684 assert_eq!(p_patched.types.as_ref().unwrap().kind, "PartialObjectMetadata");
685 assert_eq!(p_patched.types.as_ref().unwrap().api_version, "meta.k8s.io/v1");
686
687 let dp = DeleteParams::default();
689 pods.delete("busybox-kube-meta", &dp).await?.map_left(|pdel| {
690 assert_eq!(pdel.name_any(), "busybox-kube-meta");
691 });
692
693 Ok(())
694 }
695 #[tokio::test]
696 #[ignore = "needs cluster (will create a CertificateSigningRequest)"]
697 async fn csr_can_be_approved() -> Result<(), Box<dyn std::error::Error>> {
698 use crate::api::PostParams;
699 use k8s_openapi::api::certificates::v1::{
700 CertificateSigningRequest, CertificateSigningRequestCondition, CertificateSigningRequestStatus,
701 };
702
703 let csr_name = "fake";
704 let dummy_csr: CertificateSigningRequest = serde_json::from_value(json!({
705 "apiVersion": "certificates.k8s.io/v1",
706 "kind": "CertificateSigningRequest",
707 "metadata": { "name": csr_name },
708 "spec": {
709 "request": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ1ZqQ0NBVDRDQVFBd0VURVBNQTBHQTFVRUF3d0dZVzVuWld4aE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRgpBQU9DQVE4QU1JSUJDZ0tDQVFFQTByczhJTHRHdTYxakx2dHhWTTJSVlRWMDNHWlJTWWw0dWluVWo4RElaWjBOCnR2MUZtRVFSd3VoaUZsOFEzcWl0Qm0wMUFSMkNJVXBGd2ZzSjZ4MXF3ckJzVkhZbGlBNVhwRVpZM3ExcGswSDQKM3Z3aGJlK1o2MVNrVHF5SVBYUUwrTWM5T1Nsbm0xb0R2N0NtSkZNMUlMRVI3QTVGZnZKOEdFRjJ6dHBoaUlFMwpub1dtdHNZb3JuT2wzc2lHQ2ZGZzR4Zmd4eW8ybmlneFNVekl1bXNnVm9PM2ttT0x1RVF6cXpkakJ3TFJXbWlECklmMXBMWnoyalVnald4UkhCM1gyWnVVV1d1T09PZnpXM01LaE8ybHEvZi9DdS8wYk83c0x0MCt3U2ZMSU91TFcKcW90blZtRmxMMytqTy82WDNDKzBERHk5aUtwbXJjVDBnWGZLemE1dHJRSURBUUFCb0FBd0RRWUpLb1pJaHZjTgpBUUVMQlFBRGdnRUJBR05WdmVIOGR4ZzNvK21VeVRkbmFjVmQ1N24zSkExdnZEU1JWREkyQTZ1eXN3ZFp1L1BVCkkwZXpZWFV0RVNnSk1IRmQycVVNMjNuNVJsSXJ3R0xuUXFISUh5VStWWHhsdnZsRnpNOVpEWllSTmU3QlJvYXgKQVlEdUI5STZXT3FYbkFvczFqRmxNUG5NbFpqdU5kSGxpT1BjTU1oNndLaTZzZFhpVStHYTJ2RUVLY01jSVUyRgpvU2djUWdMYTk0aEpacGk3ZnNMdm1OQUxoT045UHdNMGM1dVJVejV4T0dGMUtCbWRSeEgvbUNOS2JKYjFRQm1HCkkwYitEUEdaTktXTU0xMzhIQXdoV0tkNjVoVHdYOWl4V3ZHMkh4TG1WQzg0L1BHT0tWQW9FNkpsYWFHdTlQVmkKdjlOSjVaZlZrcXdCd0hKbzZXdk9xVlA3SVFjZmg3d0drWm89Ci0tLS0tRU5EIENFUlRJRklDQVRFIFJFUVVFU1QtLS0tLQo=",
710 "signerName": "kubernetes.io/kube-apiserver-client",
711 "expirationSeconds": 86400,
712 "usages": ["client auth"]
713 }
714 }))?;
715
716 let client = Client::try_default().await?;
717 let csr: Api<CertificateSigningRequest> = Api::all(client.clone());
718 assert!(csr.create(&PostParams::default(), &dummy_csr).await.is_ok());
719
720 let approval_type = "ApprovedFake";
722 let csr_status: CertificateSigningRequestStatus = CertificateSigningRequestStatus {
723 certificate: None,
724 conditions: Some(vec![CertificateSigningRequestCondition {
725 type_: approval_type.to_string(),
726 last_update_time: None,
727 last_transition_time: None,
728 message: Some(format!("{} {}", approval_type, "by kube-rs client")),
729 reason: Some("kube-rsClient".to_string()),
730 status: "True".to_string(),
731 }]),
732 };
733 let csr_status_patch = Patch::Merge(serde_json::json!({ "status": csr_status }));
734 let _ = csr
735 .patch_approval(csr_name, &Default::default(), &csr_status_patch)
736 .await?;
737 let csr_after_approval = csr.get_approval(csr_name).await?;
738
739 assert_eq!(
740 csr_after_approval
741 .status
742 .as_ref()
743 .unwrap()
744 .conditions
745 .as_ref()
746 .unwrap()[0]
747 .type_,
748 approval_type.to_string()
749 );
750 csr.delete(csr_name, &DeleteParams::default()).await?;
751 Ok(())
752 }
753
754 #[tokio::test]
755 #[ignore = "needs cluster for ephemeral containers operations"]
756 async fn can_operate_on_ephemeral_containers() -> Result<(), Box<dyn std::error::Error>> {
757 let client = Client::try_default().await?;
758
759 let api_version = client.apiserver_version().await?;
762 if api_version.major.parse::<i32>()? < 1 || api_version.minor.parse::<i32>()? < 25 {
763 return Ok(());
764 }
765
766 let pod: Pod = serde_json::from_value(serde_json::json!({
767 "apiVersion": "v1",
768 "kind": "Pod",
769 "metadata": {
770 "name": "ephemeral-container-test",
771 "labels": { "app": "kube-rs-test" },
772 },
773 "spec": {
774 "restartPolicy": "Never",
775 "containers": [{
776 "name": "busybox",
777 "image": "busybox:stable",
778 "command": ["sh", "-c", "sleep 2"],
779 }],
780 }
781 }))?;
782
783 let pod_name = pod.name_any();
784 let pods = Api::<Pod>::default_namespaced(client);
785
786 let _ = pods
791 .delete(&pod.name_any(), &DeleteParams::default())
792 .await
793 .map(|v| v.map_left(|pdel| assert_eq!(pdel.name_any(), pod.name_any())));
794
795 match pods.create(&Default::default(), &pod).await {
798 Ok(o) => assert_eq!(pod.name_unchecked(), o.name_unchecked()),
799 Err(e) => return Err(e.into()), }
801
802 let current_ephemeral_containers = pods
803 .get_ephemeral_containers(&pod.name_any())
804 .await?
805 .spec
806 .unwrap()
807 .ephemeral_containers;
808
809 assert_eq!(current_ephemeral_containers, None);
812
813 let mut busybox_eph: EphemeralContainer = serde_json::from_value(json!(
814 {
815 "name": "myephemeralcontainer1",
816 "image": "busybox:stable",
817 "command": ["sh", "-c", "sleep 2"],
818 }
819 ))?;
820
821 let patch: Pod = serde_json::from_value(json!({
824 "metadata": { "name": pod_name },
825 "spec":{ "ephemeralContainers": [ busybox_eph ] }
826 }))?;
827
828 let current_containers = pods
829 .replace_ephemeral_containers(&pod_name, &PostParams::default(), &patch)
830 .await?
831 .spec
832 .unwrap()
833 .ephemeral_containers
834 .expect("could find ephemeral container");
835
836 assert_eq!(current_containers.len(), 1);
839 assert_eq!(current_containers[0].name, busybox_eph.name);
840 assert_eq!(current_containers[0].image, busybox_eph.image);
841 assert_eq!(current_containers[0].command, busybox_eph.command);
842
843 busybox_eph = serde_json::from_value(json!(
848 {
849 "name": "myephemeralcontainer2",
850 "image": "busybox:stable",
851 "command": ["sh", "-c", "sleep 1"],
852 }
853 ))?;
854
855 let patch: Pod =
856 serde_json::from_value(json!({ "spec": { "ephemeralContainers": [ busybox_eph ] }}))?;
857
858 let current_containers = pods
859 .patch_ephemeral_containers(&pod_name, &PatchParams::default(), &Patch::Strategic(patch))
860 .await?
861 .spec
862 .unwrap()
863 .ephemeral_containers
864 .expect("could find ephemeral container");
865
866 assert_eq!(current_containers.len(), 2);
869
870 let new_container = current_containers
871 .iter()
872 .find(|c| c.name == busybox_eph.name)
873 .expect("could find myephemeralcontainer2");
874
875 assert_eq!(new_container.image, busybox_eph.image);
878 assert_eq!(new_container.command, busybox_eph.command);
879
880 let expected_containers = current_containers;
883
884 let current_containers = pods
885 .get_ephemeral_containers(&pod.name_any())
886 .await?
887 .spec
888 .unwrap()
889 .ephemeral_containers
890 .unwrap();
891
892 assert_eq!(current_containers, expected_containers);
893
894 pods.delete(&pod.name_any(), &DeleteParams::default())
895 .await?
896 .map_left(|pdel| {
897 assert_eq!(pdel.name_any(), pod.name_any());
898 });
899
900 Ok(())
901 }
902
903 #[tokio::test]
904 #[ignore = "needs kubelet debug methods"]
905 #[cfg(feature = "kubelet-debug")]
906 async fn pod_can_exec_and_write_to_stdin_from_node_proxy() -> Result<(), Box<dyn std::error::Error>> {
907 use crate::{
908 api::{DeleteParams, ListParams, Patch, PatchParams, WatchEvent},
909 core::kubelet_debug::KubeletDebugParams,
910 };
911
912 let client = Client::try_default().await?;
913 let pods: Api<Pod> = Api::default_namespaced(client);
914
915 let p: Pod = serde_json::from_value(json!({
917 "apiVersion": "v1",
918 "kind": "Pod",
919 "metadata": {
920 "name": "busybox-kube2",
921 "labels": { "app": "kube-rs-test" },
922 },
923 "spec": {
924 "terminationGracePeriodSeconds": 1,
925 "restartPolicy": "Never",
926 "containers": [{
927 "name": "busybox",
928 "image": "busybox:stable",
929 "command": ["sh", "-c", "sleep 30"],
930 }],
931 }
932 }))?;
933
934 match pods.create(&Default::default(), &p).await {
935 Ok(o) => assert_eq!(p.name_unchecked(), o.name_unchecked()),
936 Err(crate::Error::Api(ae)) => assert_eq!(ae.code, 409), Err(e) => return Err(e.into()), }
939
940 let wp = WatchParams::default()
943 .fields(&format!("metadata.name={}", "busybox-kube2"))
944 .timeout(15);
945 let mut stream = pods.watch(&wp, "0").await?.boxed();
946 while let Some(ev) = stream.try_next().await? {
947 match ev {
948 WatchEvent::Modified(o) => {
949 let s = o.status.as_ref().expect("status exists on pod");
950 let phase = s.phase.clone().unwrap_or_default();
951 if phase == "Running" {
952 break;
953 }
954 }
955 WatchEvent::Error(e) => panic!("watch error: {e}"),
956 _ => {}
957 }
958 }
959
960 let mut config = Config::infer().await?;
961 config.accept_invalid_certs = true;
962 config.cluster_url = "https://localhost:10250".parse().unwrap();
963 let kubelet_client: Client = config.try_into()?;
964
965 {
967 let mut attached = kubelet_client
968 .kubelet_node_exec(
969 &KubeletDebugParams {
970 name: "busybox-kube2",
971 namespace: "default",
972 ..Default::default()
973 },
974 "busybox",
975 vec!["sh", "-c", "for i in $(seq 1 3); do echo $i; done"],
976 &AttachParams::default().stderr(false),
977 )
978 .await?;
979 let stdout = tokio_util::io::ReaderStream::new(attached.stdout().unwrap());
980 let out = stdout
981 .filter_map(|r| async { r.ok().and_then(|v| String::from_utf8(v.to_vec()).ok()) })
982 .collect::<Vec<_>>()
983 .await
984 .join("");
985 attached.join().await.unwrap();
986 assert_eq!(out.lines().count(), 3);
987 assert_eq!(out, "1\n2\n3\n");
988 }
989
990 {
992 use tokio::io::AsyncWriteExt;
993 let mut attached = kubelet_client
994 .kubelet_node_exec(
995 &KubeletDebugParams {
996 name: "busybox-kube2",
997 namespace: "default",
998 ..Default::default()
999 },
1000 "busybox",
1001 vec!["sh"],
1002 &AttachParams::default().stdin(true).stderr(false),
1003 )
1004 .await?;
1005 let mut stdin_writer = attached.stdin().unwrap();
1006 let mut stdout_stream = tokio_util::io::ReaderStream::new(attached.stdout().unwrap());
1007 let next_stdout = stdout_stream.next();
1008 stdin_writer.write_all(b"echo test string 1\n").await?;
1009 let stdout = String::from_utf8(next_stdout.await.unwrap().unwrap().to_vec()).unwrap();
1010 println!("{stdout}");
1011 assert_eq!(stdout, "test string 1\n");
1012
1013 stdin_writer.write_all(b"exit 1\n").await?;
1016 let status = attached.take_status().unwrap();
1017 if let Some(status) = status.await {
1018 println!("{status:?}");
1019 assert_eq!(status.status, Some("Failure".to_owned()));
1020 assert_eq!(status.reason, Some("NonZeroExitCode".to_owned()));
1021 }
1022 }
1023
1024 let dp = DeleteParams::default();
1026 pods.delete("busybox-kube2", &dp).await?.map_left(|pdel| {
1027 assert_eq!(pdel.name_unchecked(), "busybox-kube2");
1028 });
1029
1030 Ok(())
1031 }
1032}