diff --git a/kube-client/src/api/mod.rs b/kube-client/src/api/mod.rs index 9690a87e2..88342025b 100644 --- a/kube-client/src/api/mod.rs +++ b/kube-client/src/api/mod.rs @@ -14,6 +14,11 @@ mod subresource; pub use subresource::{Attach, AttachParams, Execute, Portforward}; pub use subresource::{Evict, EvictParams, Log, LogParams, ScaleSpec, ScaleStatus}; +// Ephemeral containers were stabilized in Kubernetes 1.25. +k8s_openapi::k8s_if_ge_1_25! { + pub use subresource::Ephemeral; +} + mod util; pub mod entry; diff --git a/kube-client/src/api/subresource.rs b/kube-client/src/api/subresource.rs index 4b702fde7..08f6a3078 100644 --- a/kube-client/src/api/subresource.rs +++ b/kube-client/src/api/subresource.rs @@ -1,5 +1,5 @@ use futures::AsyncBufRead; -use serde::de::DeserializeOwned; +use serde::{de::DeserializeOwned, Serialize}; use std::fmt::Debug; use crate::{ @@ -127,6 +127,160 @@ where } } +// ---------------------------------------------------------------------------- +// Ephemeral containers +// ---------------------------------------------------------------------------- + +/// Marker trait for objects that support the ephemeral containers sub resource. +pub trait Ephemeral {} + +impl Ephemeral for k8s_openapi::api::core::v1::Pod {} + +impl Api +where + K: Clone + DeserializeOwned + Ephemeral, +{ + /// Replace the ephemeral containers sub resource entirely. + /// + /// This functions in the same way as [`Api::replace`] except only `.spec.ephemeralcontainers` is replaced, everything else is ignored. + /// + /// Note that ephemeral containers may **not** be changed or removed once attached to a pod. + /// + /// + /// You way want to patch the underlying resource to gain access to the main container process, + /// see the [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/share-process-namespace/) for `sharedProcessNamespace`. + /// + /// See the Kubernetes [documentation](https://kubernetes.io/docs/concepts/workloads/pods/ephemeral-containers/#what-is-an-ephemeral-container) for more details. + /// + /// [`Api::patch_ephemeral_containers`] may be more ergonomic, as you can will avoid having to first fetch the + /// existing subresources with an approriate merge strategy, see the examples for more details. + /// + /// Example of using `replace_ephemeral_containers`: + /// + /// ```no_run + /// use k8s_openapi::api::core::v1::Pod; + /// use kube::{Api, api::PostParams}; + /// # async fn wrapper() -> Result<(), Box> { + /// # let client = kube::Client::try_default().await?; + /// let pods: Api = Api::namespaced(client, "apps"); + /// let pp = PostParams::default(); + /// + /// // Get pod object with ephemeral containers. + /// let mut mypod = pods.get_ephemeral_containers("mypod").await?; + /// + /// // If there were existing ephemeral containers, we would have to append + /// // new containers to the list before calling replace_ephemeral_containers. + /// assert_eq!(mypod.spec.as_mut().unwrap().ephemeral_containers, None); + /// + /// // Add an ephemeral container to the pod object. + /// mypod.spec.as_mut().unwrap().ephemeral_containers = Some(serde_json::from_value(serde_json::json!([ + /// { + /// "name": "myephemeralcontainer", + /// "image": "busybox:1.34.1", + /// "command": ["sh", "-c", "sleep 20"], + /// }, + /// ]))?); + /// + /// pods.replace_ephemeral_containers("mypod", &pp, &mypod).await?; + /// + /// # Ok(()) + /// # } + /// ``` + pub async fn replace_ephemeral_containers(&self, name: &str, pp: &PostParams, data: &K) -> Result + where + K: Serialize, + { + let mut req = self + .request + .replace_subresource( + "ephemeralcontainers", + name, + pp, + serde_json::to_vec(data).map_err(Error::SerdeError)?, + ) + .map_err(Error::BuildRequest)?; + req.extensions_mut().insert("replace_ephemeralcontainers"); + self.client.request::(req).await + } + + /// Patch the ephemeral containers sub resource + /// + /// Any partial object containing the ephemeral containers + /// sub resource is valid as long as the complete structure + /// for the object is present, as shown below. + /// + /// You way want to patch the underlying resource to gain access to the main container process, + /// see the [docs](https://kubernetes.io/docs/tasks/configure-pod-container/share-process-namespace/) for `sharedProcessNamespace`. + /// + /// Ephemeral containers may **not** be changed or removed once attached to a pod. + /// Therefore if the chosen merge strategy overwrites the existing ephemeral containers, + /// you will have to fetch the existing ephemeral containers first. + /// In order to append your new ephemeral containers to the existing list before patching. See some examples and + /// discussion related to merge strategies in Kubernetes + /// [here](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/#use-a-json-merge-patch-to-update-a-deployment). The example below uses a strategic merge patch which does not require + /// + /// See the `Kubernetes` [documentation](https://kubernetes.io/docs/concepts/workloads/pods/ephemeral-containers/) + /// for more information about ephemeral containers. + /// + /// + /// Example of using `patch_ephemeral_containers`: + /// + /// ```no_run + /// use kube::api::{Api, PatchParams, Patch}; + /// use k8s_openapi::api::core::v1::Pod; + /// # async fn wrapper() -> Result<(), Box> { + /// # let client = kube::Client::try_default().await?; + /// let pods: Api = Api::namespaced(client, "apps"); + /// let pp = PatchParams::default(); // stratetgic merge patch + /// + /// // Note that the strategic merge patch will concatenate the + /// // lists of ephemeral containers so we avoid having to fetch the + /// // current list and append to it manually. + /// let patch = serde_json::json!({ + /// "spec":{ + /// "ephemeralContainers": [ + /// { + /// "name": "myephemeralcontainer", + /// "image": "busybox:1.34.1", + /// "command": ["sh", "-c", "sleep 20"], + /// }, + /// ] + /// }}); + /// + /// pods.patch_ephemeral_containers("mypod", &pp, &Patch::Strategic(patch)).await?; + /// + /// # Ok(()) + /// # } + /// ``` + pub async fn patch_ephemeral_containers( + &self, + name: &str, + pp: &PatchParams, + patch: &Patch

, + ) -> Result { + let mut req = self + .request + .patch_subresource("ephemeralcontainers", name, pp, patch) + .map_err(Error::BuildRequest)?; + + req.extensions_mut().insert("patch_ephemeralcontainers"); + self.client.request::(req).await + } + + /// Get the named resource with the ephemeral containers subresource. + /// + /// This returns the whole K, with metadata and spec. + pub async fn get_ephemeral_containers(&self, name: &str) -> Result { + let mut req = self + .request + .get_subresource("ephemeralcontainers", name) + .map_err(Error::BuildRequest)?; + + req.extensions_mut().insert("get_ephemeralcontainers"); + self.client.request::(req).await + } +} + // ---------------------------------------------------------------------------- // TODO: Replace examples with owned custom resources. Bad practice to write to owned objects @@ -153,23 +307,22 @@ where /// NB: Requires that the resource has a status subresource. /// /// ```no_run - /// use kube::{api::{Api, PatchParams, Patch}, Client}; + /// use kube::api::{Api, PatchParams, Patch}; /// use k8s_openapi::api::batch::v1::Job; - /// #[tokio::main] - /// async fn main() -> Result<(), Box> { - /// let client = Client::try_default().await?; - /// let jobs: Api = Api::namespaced(client, "apps"); - /// let mut j = jobs.get("baz").await?; - /// let pp = PatchParams::default(); // json merge patch - /// let data = serde_json::json!({ - /// "status": { - /// "succeeded": 2 - /// } - /// }); - /// let o = jobs.patch_status("baz", &pp, &Patch::Merge(data)).await?; - /// assert_eq!(o.status.unwrap().succeeded, Some(2)); - /// Ok(()) - /// } + /// # async fn wrapper() -> Result<(), Box> { + /// # let client = kube::Client::try_default().await?; + /// let jobs: Api = Api::namespaced(client, "apps"); + /// let mut j = jobs.get("baz").await?; + /// let pp = PatchParams::default(); // json merge patch + /// let data = serde_json::json!({ + /// "status": { + /// "succeeded": 2 + /// } + /// }); + /// let o = jobs.patch_status("baz", &pp, &Patch::Merge(data)).await?; + /// assert_eq!(o.status.unwrap().succeeded, Some(2)); + /// # Ok(()) + /// # } /// ``` pub async fn patch_status( &self, @@ -191,18 +344,17 @@ where /// You can leave out the `.spec` entirely from the serialized output. /// /// ```no_run - /// use kube::{api::{Api, PostParams}, Client}; + /// use kube::api::{Api, PostParams}; /// use k8s_openapi::api::batch::v1::{Job, JobStatus}; - /// #[tokio::main] - /// async fn main() -> Result<(), Box> { - /// let client = Client::try_default().await?; - /// let jobs: Api = Api::namespaced(client, "apps"); - /// let mut o = jobs.get_status("baz").await?; // retrieve partial object - /// o.status = Some(JobStatus::default()); // update the job part - /// let pp = PostParams::default(); - /// let o = jobs.replace_status("baz", &pp, serde_json::to_vec(&o)?).await?; - /// Ok(()) - /// } + /// # async fn wrapper() -> Result<(), Box> { + /// # let client = kube::Client::try_default().await?; + /// let jobs: Api = Api::namespaced(client, "apps"); + /// let mut o = jobs.get_status("baz").await?; // retrieve partial object + /// o.status = Some(JobStatus::default()); // update the job part + /// let pp = PostParams::default(); + /// let o = jobs.replace_status("baz", &pp, serde_json::to_vec(&o)?).await?; + /// # Ok(()) + /// # } /// ``` pub async fn replace_status(&self, name: &str, pp: &PostParams, data: Vec) -> Result { let mut req = self diff --git a/kube-client/src/lib.rs b/kube-client/src/lib.rs index 911c41056..d662ff4fa 100644 --- a/kube-client/src/lib.rs +++ b/kube-client/src/lib.rs @@ -137,9 +137,9 @@ mod test { Api, Client, Config, ResourceExt, }; use futures::{AsyncBufRead, AsyncBufReadExt, StreamExt, TryStreamExt}; - use k8s_openapi::api::core::v1::Pod; + use k8s_openapi::api::core::v1::{EphemeralContainer, Pod, PodSpec}; use kube_core::{ - params::{DeleteParams, Patch, WatchParams}, + params::{DeleteParams, Patch, PatchParams, PostParams, WatchParams}, response::StatusSummary, }; use serde_json::json; @@ -610,4 +610,153 @@ mod test { csr.delete(csr_name, &DeleteParams::default()).await?; Ok(()) } + + #[tokio::test] + #[ignore = "needs cluster for ephemeral containers operations"] + async fn can_operate_on_ephemeral_containers() -> Result<(), Box> { + let client = Client::try_default().await?; + + // Ephemeral containers were stabilized in Kubernetes v1.25. + // This test therefore exits early if the current cluster version is older than v1.25. + let api_version = client.apiserver_version().await?; + if api_version.major.parse::()? < 1 || api_version.minor.parse::()? < 25 { + return Ok(()); + } + + let pod: Pod = serde_json::from_value(serde_json::json!({ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "ephemeral-container-test", + "labels": { "app": "kube-rs-test" }, + }, + "spec": { + "restartPolicy": "Never", + "containers": [{ + "name": "busybox", + "image": "busybox:1.34.1", + "command": ["sh", "-c", "sleep 2"], + }], + } + }))?; + + let pod_name = pod.name_any(); + let pods = Api::::default_namespaced(client); + + // If cleanup failed and a pod already exists, we attempt to remove it + // before proceeding. This is important as ephemeral containers can't + // be removed from a Pod's spec. Therefore this test must start with a fresh + // Pod every time. + let _ = pods + .delete(&pod.name_any(), &DeleteParams::default()) + .await + .map(|v| v.map_left(|pdel| assert_eq!(pdel.name_any(), pod.name_any()))); + + // Ephemeral containes can only be applied to a running pod, so one must + // be created before any operations are tested. + match pods.create(&Default::default(), &pod).await { + Ok(o) => assert_eq!(pod.name_unchecked(), o.name_unchecked()), + Err(e) => return Err(e.into()), // any other case if a failure + } + + let current_ephemeral_containers = pods + .get_ephemeral_containers(&pod.name_any()) + .await? + .spec + .unwrap() + .ephemeral_containers; + + // We expect no ephemeral containers initially, get_ephemeral_containers should + // reflect that. + assert_eq!(current_ephemeral_containers, None); + + let mut busybox_eph: EphemeralContainer = serde_json::from_value(json!( + { + "name": "myephemeralcontainer1", + "image": "busybox:1.34.1", + "command": ["sh", "-c", "sleep 2"], + } + ))?; + + // Attempt to replace ephemeral containers. + + let patch: Pod = serde_json::from_value(json!({ + "metadata": { "name": pod_name }, + "spec":{ "ephemeralContainers": [ busybox_eph ] } + }))?; + + let current_containers = pods + .replace_ephemeral_containers(&pod_name, &PostParams::default(), &patch) + .await? + .spec + .unwrap() + .ephemeral_containers + .expect("could find ephemeral container"); + + // Note that we can't compare the whole ephemeral containers object, as some fields + // are set by the cluster. We therefore compare the fields specified in the patch. + assert_eq!(current_containers.len(), 1); + assert_eq!(current_containers[0].name, busybox_eph.name); + assert_eq!(current_containers[0].image, busybox_eph.image); + assert_eq!(current_containers[0].command, busybox_eph.command); + + // Attempt to patch ephemeral containers. + + // The new ephemeral container will have different values from the + // first to ensure we can test for its presence. + busybox_eph = serde_json::from_value(json!( + { + "name": "myephemeralcontainer2", + "image": "busybox:1.35.0", + "command": ["sh", "-c", "sleep 1"], + } + ))?; + + let patch: Pod = + serde_json::from_value(json!({ "spec": { "ephemeralContainers": [ busybox_eph ] }}))?; + + let current_containers = pods + .patch_ephemeral_containers(&pod_name, &PatchParams::default(), &Patch::Strategic(patch)) + .await? + .spec + .unwrap() + .ephemeral_containers + .expect("could find ephemeral container"); + + // There should only be 2 ephemeral containers at this point, + // one from each patch + assert_eq!(current_containers.len(), 2); + + let new_container = current_containers + .iter() + .find(|c| c.name == busybox_eph.name) + .expect("could find myephemeralcontainer2"); + + // Note that we can't compare the whole ephemeral container object, as some fields + // get set in the cluster. We therefore compare the fields specified in the patch. + assert_eq!(new_container.image, busybox_eph.image); + assert_eq!(new_container.command, busybox_eph.command); + + // Attempt to get ephemeral containers. + + let expected_containers = current_containers; + + let current_containers = pods + .get_ephemeral_containers(&pod.name_any()) + .await? + .spec + .unwrap() + .ephemeral_containers + .unwrap(); + + assert_eq!(current_containers, expected_containers); + + pods.delete(&pod.name_any(), &DeleteParams::default()) + .await? + .map_left(|pdel| { + assert_eq!(pdel.name_any(), pod.name_any()); + }); + + Ok(()) + } }