diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 2de79ec87..27732bc56 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -10,10 +10,23 @@ license.workspace = true release = false [features] -default = ["rustls-tls", "kubederive", "ws", "latest", "socks5", "runtime", "refresh"] +default = [ + "rustls-tls", + "kubederive", + "ws", + "latest", + "socks5", + "runtime", + "refresh", +] kubederive = ["kube/derive"] openssl-tls = ["kube/client", "kube/openssl-tls", "kube/unstable-client"] -rustls-tls = ["kube/client", "kube/rustls-tls", "kube/ring", "kube/unstable-client"] +rustls-tls = [ + "kube/client", + "kube/rustls-tls", + "kube/ring", + "kube/unstable-client", +] runtime = ["kube/runtime", "kube/unstable-runtime"] schemars-preserve-order = ["schemars/preserve_order"] socks5 = ["kube/socks5"] @@ -29,7 +42,9 @@ garde = { version = "0.22.0", default-features = false, features = ["derive"] } anyhow.workspace = true futures = { workspace = true, features = ["async-await"] } jsonpath-rust.workspace = true -kube = { path = "../kube", version = "^2.0.1", default-features = false, features = ["admission"] } +kube = { path = "../kube", version = "^2.0.1", default-features = false, features = [ + "admission", +] } kube-derive = { path = "../kube-derive", version = "^2.0.1", default-features = false } # only needed to opt out of schema k8s-openapi.workspace = true serde = { workspace = true, features = ["derive"] } @@ -50,10 +65,19 @@ json-patch.workspace = true tower = { workspace = true, features = ["limit"] } tower-http = { workspace = true, features = ["trace", "decompression-gzip"] } hyper = { workspace = true, features = ["client", "http1"] } -hyper-util = { workspace = true, features = ["client-legacy", "http1", "tokio", "tracing"] } +hyper-util = { workspace = true, features = [ + "client-legacy", + "http1", + "tokio", + "tracing", +] } thiserror.workspace = true backon.workspace = true -clap = { version = "4.0", default-features = false, features = ["std", "cargo", "derive"] } +clap = { version = "4.0", default-features = false, features = [ + "std", + "cargo", + "derive", +] } edit = "0.1.3" tokio-stream = { version = "0.1.9", features = ["net"] } crossterm = "0.29.0" @@ -175,6 +199,10 @@ path = "pod_portforward_hyper_http.rs" name = "pod_portforward_bind" path = "pod_portforward_bind.rs" +[[example]] +name = "pod_resize" +path = "pod_resize.rs" + [[example]] name = "pod_reflector" path = "pod_reflector.rs" diff --git a/examples/pod_resize.rs b/examples/pod_resize.rs new file mode 100644 index 000000000..49a395ecd --- /dev/null +++ b/examples/pod_resize.rs @@ -0,0 +1,131 @@ +use k8s_openapi::api::core::v1::Pod; +use kube::{ + api::{Api, DeleteParams, Patch, PatchParams, PostParams, ResourceExt}, + runtime::wait::{await_condition, conditions::is_pod_running}, + Client, +}; +use tracing::*; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + let client = Client::try_default().await?; + + let pods: Api = Api::default_namespaced(client); + + // Create a sample pod with resource limits + info!("Creating pod with initial resource requirements"); + let pod: Pod = serde_json::from_value(serde_json::json!({ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { "name": "resize-demo" }, + "spec": { + "containers": [{ + "name": "app", + "image": "nginx:1.14.2", + "resources": { + "requests": { + "cpu": "100m", + "memory": "128Mi" + }, + "limits": { + "cpu": "200m", + "memory": "256Mi" + } + } + }] + } + }))?; + + let pp = PostParams::default(); + pods.create(&pp, &pod).await?; + + // Wait for pod to be running + info!("Waiting for pod to be running..."); + let running = await_condition(pods.clone(), "resize-demo", is_pod_running()); + let _ = tokio::time::timeout(std::time::Duration::from_secs(30), running).await?; + + // Example 1: Using get_resize to view current state + info!("Example 1: Getting pod resize subresource"); + let current = pods.get_resize("resize-demo").await?; + if let Some(spec) = ¤t.spec { + if let Some(container) = spec.containers.first() { + info!("Current resources: {:?}", container.resources); + } + } + + // Example 2: Using patch_resize to update resources + info!("Example 2: Patching pod resources using resize subresource"); + let patch = serde_json::json!({ + "spec": { + "containers": [{ + "name": "app", + "resources": { + "requests": { + "cpu": "150m", + "memory": "256Mi" + }, + "limits": { + "cpu": "300m", + "memory": "512Mi" + } + } + }] + } + }); + + let patch_params = PatchParams::default(); + match pods + .patch_resize("resize-demo", &patch_params, &Patch::Strategic(patch)) + .await + { + Ok(resized) => { + info!("Successfully patched pod: {}", resized.name_any()); + if let Some(spec) = resized.spec { + if let Some(container) = spec.containers.first() { + info!("Updated resources via patch: {:?}", container.resources); + } + } + } + Err(e) => { + error!("Failed to patch resize pod: {}", e); + } + } + + // Example 3: Using replace_resize + info!("Example 3: Using replace_resize method"); + let mut current_pod = pods.get_resize("resize-demo").await?; + + if let Some(spec) = &mut current_pod.spec { + if let Some(container) = spec.containers.get_mut(0) { + if let Some(resources) = &mut container.resources { + // Update memory request + if let Some(requests) = &mut resources.requests { + requests.insert( + "memory".to_string(), + k8s_openapi::apimachinery::pkg::api::resource::Quantity("384Mi".to_string()), + ); + } + } + } + } + + match pods.replace_resize("resize-demo", &pp, ¤t_pod).await { + Ok(resized) => { + info!("Pod resized via replace: {}", resized.name_any()); + if let Some(spec) = resized.spec { + if let Some(container) = spec.containers.first() { + info!("Final resources via replace: {:?}", container.resources); + } + } + } + Err(e) => error!("Failed to replace_resize: {}", e), + } + + // Cleanup + info!("Cleaning up"); + let dp = DeleteParams::default(); + pods.delete("resize-demo", &dp).await?; + + Ok(()) +} diff --git a/kube-client/src/api/mod.rs b/kube-client/src/api/mod.rs index bb659d80f..5d6006f6b 100644 --- a/kube-client/src/api/mod.rs +++ b/kube-client/src/api/mod.rs @@ -9,6 +9,8 @@ use std::fmt::Debug; #[cfg(feature = "ws")] pub use portforward::Portforwarder; mod subresource; +#[cfg_attr(docsrs, doc(cfg(feature = "k8s_if_ge_1_33")))] +pub use subresource::Resize; #[cfg(feature = "ws")] #[cfg_attr(docsrs, doc(cfg(feature = "ws")))] pub use subresource::{Attach, AttachParams, Ephemeral, Execute, Portforward}; diff --git a/kube-client/src/api/subresource.rs b/kube-client/src/api/subresource.rs index 11617545f..abd9f43e5 100644 --- a/kube-client/src/api/subresource.rs +++ b/kube-client/src/api/subresource.rs @@ -283,6 +283,154 @@ where } } +// ---------------------------------------------------------------------------- +// Resize subresource +// ---------------------------------------------------------------------------- + +/// Marker trait for objects that support the resize sub resource. +/// +/// The resize subresource allows updating container resource requests/limits +/// without restarting the pod. This is available in Kubernetes 1.33+. +/// +/// See [`Api::get_resize`], [`Api::patch_resize`], and [`Api::replace_resize`]. +/// +/// See the Kubernetes [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/) +/// and [limitations](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/#limitations) +/// for more details. +#[cfg_attr(docsrs, doc(cfg(feature = "k8s_if_ge_1_33")))] +pub trait Resize {} + +k8s_openapi::k8s_if_ge_1_33! { + impl Resize for k8s_openapi::api::core::v1::Pod {} +} + +k8s_openapi::k8s_if_ge_1_33! { + impl Api + where + K: Clone + DeserializeOwned + Resize, + { + /// Get the named resource with the resize subresource. + /// + /// This returns the whole Pod object with current resource allocations. + /// + /// See the Kubernetes [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/) + /// and [limitations](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/#limitations) + /// for more details. + pub async fn get_resize(&self, name: &str) -> Result { + let mut req = self + .request + .get_subresource("resize", name) + .map_err(Error::BuildRequest)?; + req.extensions_mut().insert("get_resize"); + self.client.request::(req).await + } + + /// Patch the resize sub resource. + /// + /// This allows you to update specific container resource requirements + /// without fetching the entire Pod object first. + /// + /// Note that only certain container resource fields can be modified. See the + /// [limitations](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/#limitations) + /// for details on what can be changed. + /// + /// # Example + /// + /// ```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, "default"); + /// let pp = PatchParams::default(); + /// + /// let patch = serde_json::json!({ + /// "spec": { + /// "containers": [{ + /// "name": "mycontainer", + /// "resources": { + /// "requests": { + /// "cpu": "200m", + /// "memory": "512Mi" + /// } + /// } + /// }] + /// } + /// }); + /// + /// pods.patch_resize("mypod", &pp, &Patch::Strategic(patch)).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn patch_resize( + &self, + name: &str, + pp: &PatchParams, + patch: &Patch

, + ) -> Result { + let mut req = self + .request + .patch_subresource("resize", name, pp, patch) + .map_err(Error::BuildRequest)?; + req.extensions_mut().insert("patch_resize"); + self.client.request::(req).await + } + + /// Replace the resize sub resource entirely. + /// + /// This works similarly to [`Api::replace`] but uses the resize subresource. + /// Takes a full Pod object with updated container resource requirements. + /// + /// Note that only certain container resource fields can be modified. See the + /// [limitations](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/#limitations) + /// for details on what can be changed. + /// + /// # Example + /// + /// ```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, "default"); + /// let pp = PostParams::default(); + /// + /// // Get current pod + /// let mut pod = pods.get("mypod").await?; + /// + /// // Modify resource requirements + /// if let Some(spec) = &mut pod.spec { + /// if let Some(container) = spec.containers.get_mut(0) { + /// if let Some(resources) = &mut container.resources { + /// // Update CPU/memory limits or requests + /// // ... + /// } + /// } + /// } + /// + /// pods.replace_resize("mypod", &pp, &pod).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn replace_resize(&self, name: &str, pp: &PostParams, data: &K) -> Result + where + K: Serialize, + { + let mut req = self + .request + .replace_subresource( + "resize", + name, + pp, + serde_json::to_vec(data).map_err(Error::SerdeError)?, + ) + .map_err(Error::BuildRequest)?; + req.extensions_mut().insert("replace_resize"); + self.client.request::(req).await + } + } +} + // ---------------------------------------------------------------------------- // TODO: Replace examples with owned custom resources. Bad practice to write to owned objects @@ -610,3 +758,19 @@ where Ok(Portforwarder::new(connection.into_stream(), ports)) } } + +// ---------------------------------------------------------------------------- +// Resize subresource tests +// ---------------------------------------------------------------------------- + +#[test] +fn resize_path() { + use crate::api::{Request, Resource}; + use k8s_openapi::api::core::v1 as corev1; + + k8s_openapi::k8s_if_ge_1_33! { + let url = corev1::Pod::url_path(&(), Some("ns")); + let req = Request::new(url).get_subresource("resize", "mypod").unwrap(); + assert_eq!(req.uri(), "/api/v1/namespaces/ns/pods/mypod/resize"); + } +}