Skip to main content

kube_client/
lib.rs

1//! Crate for interacting with the Kubernetes API
2//!
3//! This crate includes the tools for manipulating Kubernetes resources as
4//! well as keeping track of those resources as they change over time
5//!
6//! # Example
7//!
8//! The following example will create a [`Pod`](k8s_openapi::api::core::v1::Pod)
9//! and then watch for it to become available using a manual [`Api::watch`] call.
10//!
11//! ```rust,no_run
12//! use futures::{StreamExt, TryStreamExt};
13//! use kube_client::api::{Api, ResourceExt, ListParams, PatchParams, Patch};
14//! use kube_client::Client;
15//! use k8s_openapi::api::core::v1::Pod;
16//!
17//! #[tokio::main]
18//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
19//!     // Read the environment to find config for kube client.
20//!     // Note that this tries an in-cluster configuration first,
21//!     // then falls back on a kubeconfig file.
22//!     let client = Client::try_default().await?;
23//!
24//!     // Interact with pods in the configured namespace with the typed interface from k8s-openapi
25//!     let pods: Api<Pod> = Api::default_namespaced(client);
26//!
27//!     // Create a Pod (cheating here with json, but it has to validate against the type):
28//!     let patch: Pod = serde_json::from_value(serde_json::json!({
29//!         "apiVersion": "v1",
30//!         "kind": "Pod",
31//!         "metadata": {
32//!             "name": "my-pod"
33//!         },
34//!         "spec": {
35//!             "containers": [
36//!                 {
37//!                     "name": "my-container",
38//!                     "image": "myregistry.azurecr.io/hello-world:v1",
39//!                 },
40//!             ],
41//!         }
42//!     }))?;
43//!
44//!     // Apply the Pod via server-side apply
45//!     let params = PatchParams::apply("myapp");
46//!     let result = pods.patch("my-pod", &params, &Patch::Apply(&patch)).await?;
47//!
48//!     // List pods in the configured namespace
49//!     for p in pods.list(&ListParams::default()).await? {
50//!         println!("found pod {}", p.name_any());
51//!     }
52//!
53//!     Ok(())
54//! }
55//! ```
56//!
57//! For more details, see:
58//!
59//! - [`Client`](crate::client) for the extensible Kubernetes client
60//! - [`Config`](crate::config) for the Kubernetes config abstraction
61//! - [`Api`](crate::Api) for the generic api methods available on Kubernetes resources
62//! - [k8s-openapi](https://docs.rs/k8s-openapi) for how to create typed kubernetes objects directly
63#![cfg_attr(docsrs, feature(doc_cfg))]
64// Nightly clippy (0.1.64) considers Drop a side effect, see https://github.com/rust-lang/rust-clippy/issues/9608
65#![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    /// Convenient alias for `Result<T, Error>`
119    pub type Result<T, E = Error> = std::result::Result<T, E>;
120}
121
122pub use crate::core::{CustomResourceExt, Resource, ResourceExt};
123/// Re-exports from kube_core
124pub use kube_core as core;
125
126// Tests that require a cluster and the complete feature set
127// Can be run with `cargo test -p kube-client --lib features=rustls-tls,ws -- --ignored`
128#[cfg(all(feature = "client", feature = "config"))]
129#[cfg(test)]
130#[allow(unused_imports)] // varying test imports depending on feature
131mod 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    // hard disabled test atm due to k3d rustls issues: https://github.com/kube-rs/kube/issues?q=is%3Aopen+is%3Aissue+label%3Arustls
147    #[allow(dead_code)]
148    // #[tokio::test]
149    #[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        // Test /apis aggregated discovery
203        let apis_discovery = client.list_api_groups_aggregated().await?;
204        assert!(!apis_discovery.items.is_empty(), "should have API groups");
205
206        // Find the apps group
207        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        // Check that deployments resource exists in apps/v1
217        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        // Test /api aggregated discovery (core group)
237        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        // Check that pods resource exists
245        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        // Test Discovery::run_aggregated()
268        let discovery = Discovery::new(client.clone()).run_aggregated().await?;
269
270        // Should have discovered groups
271        assert!(discovery.groups().count() > 0, "should have discovered groups");
272
273        // Should have core group
274        assert!(discovery.has_group(""), "should have core group");
275
276        // Should have apps group
277        assert!(discovery.has_group("apps"), "should have apps group");
278
279        // Check that we can find deployments in apps group
280        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        // Check that we can find pods in core group
286        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        // create busybox pod that's alive for at most 30s
303        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), // if we failed to clean-up
325            Err(e) => return Err(e.into()),                         // any other case if a failure
326        }
327
328        // Manual watch-api for it to become ready
329        // NB: don't do this; using conditions (see pod_api example) is easier and less error prone
330        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            // can debug format watch event
336            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        // Verify we can get it
351        let mut pod = pods.get("busybox-kube1").await?;
352        assert_eq!(p.spec.as_ref().unwrap().containers[0].name, "busybox");
353
354        // verify replace with explicit resource version
355        // NB: don't do this; use server side apply
356        {
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        // Delete it
366        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        // create busybox pod that's alive for at most 30s
385        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), // if we failed to clean-up
406            Err(e) => return Err(e.into()),                         // any other case if a failure
407        }
408
409        // Manual watch-api for it to become ready
410        // NB: don't do this; using conditions (see pod_api example) is easier and less error prone
411        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        // Verify exec works and we can get the output
436        {
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        // Verify we read from stdout after stdin is closed.
456        {
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            // Make a connection so we can determine if the K8s cluster supports stream closing.
462            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            // This only works is the cluster supports protocol version v5.channel.k8s.io
467            // Skip for older protocols.
468            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                // AttachedProcess resolves with status object.
477                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        // Verify we can write to Stdin
490        {
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            // AttachedProcess resolves with status object.
507            // Send `exit 1` to get a failure status.
508            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        // Delete it
518        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        // create busybox pod that's alive for at most 30s
538        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), // if we failed to clean-up
559            Err(e) => return Err(e.into()),                         // any other case if a failure
560        }
561
562        // Manual watch-api for it to become ready
563        // NB: don't do this; using conditions (see pod_api example) is easier and less error prone
564        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        // Get current list of logs
583        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        // wait for container to finish
590        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        // individual logs may or may not buffer
596        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        // evict the pod
603        let ep = EvictParams::default();
604        let eres = pods.evict("busybox-kube3", &ep).await?;
605        assert_eq!(eres.code, 201); // created
606        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        // create busybox pod that's alive for at most 30s
624        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), // if we failed to clean-up
645            Err(e) => return Err(e.into()),                         // any other case if a failure
646        }
647
648        // Test we can get a pod as a PartialObjectMeta and convert to
649        // ObjectMeta
650        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        // Test we can get a list of PartialObjectMeta for pods
658        let p_list = pods.list_metadata(&ListParams::default()).await?;
659
660        // Find only pod we are concerned with in this test and fail eagerly if
661        // name doesn't exist
662        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        // Attempt to patch pod metadata
673        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        // Clean-up
688        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        // Patch the approval and approve the CSR
721        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        // Ephemeral containers were stabilized in Kubernetes v1.25.
760        // This test therefore exits early if the current cluster version is older than v1.25.
761        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        // If cleanup failed and a pod already exists, we attempt to remove it
787        // before proceeding. This is important as ephemeral containers can't
788        // be removed from a Pod's spec. Therefore this test must start with a fresh
789        // Pod every time.
790        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        // Ephemeral containers can only be applied to a running pod, so one must
796        // be created before any operations are tested.
797        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()), // any other case if a failure
800        }
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        // We expect no ephemeral containers initially, get_ephemeral_containers should
810        // reflect that.
811        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        // Attempt to replace ephemeral containers.
822
823        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        // Note that we can't compare the whole ephemeral containers object, as some fields
837        // are set by the cluster. We therefore compare the fields specified in the patch.
838        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        // Attempt to patch ephemeral containers.
844
845        // The new ephemeral container will have different values from the
846        // first to ensure we can test for its presence.
847        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        // There should only be 2 ephemeral containers at this point,
867        // one from each patch
868        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        // Note that we can't compare the whole ephemeral container object, as some fields
876        // get set in the cluster. We therefore compare the fields specified in the patch.
877        assert_eq!(new_container.image, busybox_eph.image);
878        assert_eq!(new_container.command, busybox_eph.command);
879
880        // Attempt to get ephemeral containers.
881
882        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        // create busybox pod that's alive for at most 30s
916        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), // if we failed to clean-up
937            Err(e) => return Err(e.into()),                         // any other case if a failure
938        }
939
940        // Manual watch-api for it to become ready
941        // NB: don't do this; using conditions (see pod_api example) is easier and less error prone
942        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        // Verify exec works and we can get the output
966        {
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        // Verify we can write to Stdin
991        {
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            // AttachedProcess resolves with status object.
1014            // Send `exit 1` to get a failure status.
1015            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        // Delete it
1025        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}