Skip to content

Commit 1dfbf1d

Browse files
authored
Merge pull request #1851 from hugoponthieu/feat-resize-subresources
Add `Resize` subresource for `Pod`
2 parents 9a584df + de3b4f7 commit 1dfbf1d

File tree

4 files changed

+330
-5
lines changed

4 files changed

+330
-5
lines changed

examples/Cargo.toml

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,23 @@ license.workspace = true
1010
release = false
1111

1212
[features]
13-
default = ["rustls-tls", "kubederive", "ws", "latest", "socks5", "runtime", "refresh"]
13+
default = [
14+
"rustls-tls",
15+
"kubederive",
16+
"ws",
17+
"latest",
18+
"socks5",
19+
"runtime",
20+
"refresh",
21+
]
1422
kubederive = ["kube/derive"]
1523
openssl-tls = ["kube/client", "kube/openssl-tls", "kube/unstable-client"]
16-
rustls-tls = ["kube/client", "kube/rustls-tls", "kube/ring", "kube/unstable-client"]
24+
rustls-tls = [
25+
"kube/client",
26+
"kube/rustls-tls",
27+
"kube/ring",
28+
"kube/unstable-client",
29+
]
1730
runtime = ["kube/runtime", "kube/unstable-runtime"]
1831
schemars-preserve-order = ["schemars/preserve_order"]
1932
socks5 = ["kube/socks5"]
@@ -29,7 +42,9 @@ garde = { version = "0.22.0", default-features = false, features = ["derive"] }
2942
anyhow.workspace = true
3043
futures = { workspace = true, features = ["async-await"] }
3144
jsonpath-rust.workspace = true
32-
kube = { path = "../kube", version = "^2.0.1", default-features = false, features = ["admission"] }
45+
kube = { path = "../kube", version = "^2.0.1", default-features = false, features = [
46+
"admission",
47+
] }
3348
kube-derive = { path = "../kube-derive", version = "^2.0.1", default-features = false } # only needed to opt out of schema
3449
k8s-openapi.workspace = true
3550
serde = { workspace = true, features = ["derive"] }
@@ -50,10 +65,19 @@ json-patch.workspace = true
5065
tower = { workspace = true, features = ["limit"] }
5166
tower-http = { workspace = true, features = ["trace", "decompression-gzip"] }
5267
hyper = { workspace = true, features = ["client", "http1"] }
53-
hyper-util = { workspace = true, features = ["client-legacy", "http1", "tokio", "tracing"] }
68+
hyper-util = { workspace = true, features = [
69+
"client-legacy",
70+
"http1",
71+
"tokio",
72+
"tracing",
73+
] }
5474
thiserror.workspace = true
5575
backon.workspace = true
56-
clap = { version = "4.0", default-features = false, features = ["std", "cargo", "derive"] }
76+
clap = { version = "4.0", default-features = false, features = [
77+
"std",
78+
"cargo",
79+
"derive",
80+
] }
5781
edit = "0.1.3"
5882
tokio-stream = { version = "0.1.9", features = ["net"] }
5983
crossterm = "0.29.0"
@@ -175,6 +199,10 @@ path = "pod_portforward_hyper_http.rs"
175199
name = "pod_portforward_bind"
176200
path = "pod_portforward_bind.rs"
177201

202+
[[example]]
203+
name = "pod_resize"
204+
path = "pod_resize.rs"
205+
178206
[[example]]
179207
name = "pod_reflector"
180208
path = "pod_reflector.rs"

examples/pod_resize.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use k8s_openapi::api::core::v1::Pod;
2+
use kube::{
3+
api::{Api, DeleteParams, Patch, PatchParams, PostParams, ResourceExt},
4+
runtime::wait::{await_condition, conditions::is_pod_running},
5+
Client,
6+
};
7+
use tracing::*;
8+
9+
#[tokio::main]
10+
async fn main() -> anyhow::Result<()> {
11+
tracing_subscriber::fmt::init();
12+
let client = Client::try_default().await?;
13+
14+
let pods: Api<Pod> = Api::default_namespaced(client);
15+
16+
// Create a sample pod with resource limits
17+
info!("Creating pod with initial resource requirements");
18+
let pod: Pod = serde_json::from_value(serde_json::json!({
19+
"apiVersion": "v1",
20+
"kind": "Pod",
21+
"metadata": { "name": "resize-demo" },
22+
"spec": {
23+
"containers": [{
24+
"name": "app",
25+
"image": "nginx:1.14.2",
26+
"resources": {
27+
"requests": {
28+
"cpu": "100m",
29+
"memory": "128Mi"
30+
},
31+
"limits": {
32+
"cpu": "200m",
33+
"memory": "256Mi"
34+
}
35+
}
36+
}]
37+
}
38+
}))?;
39+
40+
let pp = PostParams::default();
41+
pods.create(&pp, &pod).await?;
42+
43+
// Wait for pod to be running
44+
info!("Waiting for pod to be running...");
45+
let running = await_condition(pods.clone(), "resize-demo", is_pod_running());
46+
let _ = tokio::time::timeout(std::time::Duration::from_secs(30), running).await?;
47+
48+
// Example 1: Using get_resize to view current state
49+
info!("Example 1: Getting pod resize subresource");
50+
let current = pods.get_resize("resize-demo").await?;
51+
if let Some(spec) = &current.spec {
52+
if let Some(container) = spec.containers.first() {
53+
info!("Current resources: {:?}", container.resources);
54+
}
55+
}
56+
57+
// Example 2: Using patch_resize to update resources
58+
info!("Example 2: Patching pod resources using resize subresource");
59+
let patch = serde_json::json!({
60+
"spec": {
61+
"containers": [{
62+
"name": "app",
63+
"resources": {
64+
"requests": {
65+
"cpu": "150m",
66+
"memory": "256Mi"
67+
},
68+
"limits": {
69+
"cpu": "300m",
70+
"memory": "512Mi"
71+
}
72+
}
73+
}]
74+
}
75+
});
76+
77+
let patch_params = PatchParams::default();
78+
match pods
79+
.patch_resize("resize-demo", &patch_params, &Patch::Strategic(patch))
80+
.await
81+
{
82+
Ok(resized) => {
83+
info!("Successfully patched pod: {}", resized.name_any());
84+
if let Some(spec) = resized.spec {
85+
if let Some(container) = spec.containers.first() {
86+
info!("Updated resources via patch: {:?}", container.resources);
87+
}
88+
}
89+
}
90+
Err(e) => {
91+
error!("Failed to patch resize pod: {}", e);
92+
}
93+
}
94+
95+
// Example 3: Using replace_resize
96+
info!("Example 3: Using replace_resize method");
97+
let mut current_pod = pods.get_resize("resize-demo").await?;
98+
99+
if let Some(spec) = &mut current_pod.spec {
100+
if let Some(container) = spec.containers.get_mut(0) {
101+
if let Some(resources) = &mut container.resources {
102+
// Update memory request
103+
if let Some(requests) = &mut resources.requests {
104+
requests.insert(
105+
"memory".to_string(),
106+
k8s_openapi::apimachinery::pkg::api::resource::Quantity("384Mi".to_string()),
107+
);
108+
}
109+
}
110+
}
111+
}
112+
113+
match pods.replace_resize("resize-demo", &pp, &current_pod).await {
114+
Ok(resized) => {
115+
info!("Pod resized via replace: {}", resized.name_any());
116+
if let Some(spec) = resized.spec {
117+
if let Some(container) = spec.containers.first() {
118+
info!("Final resources via replace: {:?}", container.resources);
119+
}
120+
}
121+
}
122+
Err(e) => error!("Failed to replace_resize: {}", e),
123+
}
124+
125+
// Cleanup
126+
info!("Cleaning up");
127+
let dp = DeleteParams::default();
128+
pods.delete("resize-demo", &dp).await?;
129+
130+
Ok(())
131+
}

kube-client/src/api/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use std::fmt::Debug;
99
#[cfg(feature = "ws")] pub use portforward::Portforwarder;
1010

1111
mod subresource;
12+
#[cfg_attr(docsrs, doc(cfg(feature = "k8s_if_ge_1_33")))]
13+
pub use subresource::Resize;
1214
#[cfg(feature = "ws")]
1315
#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
1416
pub use subresource::{Attach, AttachParams, Ephemeral, Execute, Portforward};

kube-client/src/api/subresource.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,154 @@ where
283283
}
284284
}
285285

286+
// ----------------------------------------------------------------------------
287+
// Resize subresource
288+
// ----------------------------------------------------------------------------
289+
290+
/// Marker trait for objects that support the resize sub resource.
291+
///
292+
/// The resize subresource allows updating container resource requests/limits
293+
/// without restarting the pod. This is available in Kubernetes 1.33+.
294+
///
295+
/// See [`Api::get_resize`], [`Api::patch_resize`], and [`Api::replace_resize`].
296+
///
297+
/// See the Kubernetes [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/)
298+
/// and [limitations](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/#limitations)
299+
/// for more details.
300+
#[cfg_attr(docsrs, doc(cfg(feature = "k8s_if_ge_1_33")))]
301+
pub trait Resize {}
302+
303+
k8s_openapi::k8s_if_ge_1_33! {
304+
impl Resize for k8s_openapi::api::core::v1::Pod {}
305+
}
306+
307+
k8s_openapi::k8s_if_ge_1_33! {
308+
impl<K> Api<K>
309+
where
310+
K: Clone + DeserializeOwned + Resize,
311+
{
312+
/// Get the named resource with the resize subresource.
313+
///
314+
/// This returns the whole Pod object with current resource allocations.
315+
///
316+
/// See the Kubernetes [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/)
317+
/// and [limitations](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/#limitations)
318+
/// for more details.
319+
pub async fn get_resize(&self, name: &str) -> Result<K> {
320+
let mut req = self
321+
.request
322+
.get_subresource("resize", name)
323+
.map_err(Error::BuildRequest)?;
324+
req.extensions_mut().insert("get_resize");
325+
self.client.request::<K>(req).await
326+
}
327+
328+
/// Patch the resize sub resource.
329+
///
330+
/// This allows you to update specific container resource requirements
331+
/// without fetching the entire Pod object first.
332+
///
333+
/// Note that only certain container resource fields can be modified. See the
334+
/// [limitations](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/#limitations)
335+
/// for details on what can be changed.
336+
///
337+
/// # Example
338+
///
339+
/// ```no_run
340+
/// use kube::api::{Api, PatchParams, Patch};
341+
/// use k8s_openapi::api::core::v1::Pod;
342+
/// # async fn wrapper() -> Result<(), Box<dyn std::error::Error>> {
343+
/// # let client = kube::Client::try_default().await?;
344+
/// let pods: Api<Pod> = Api::namespaced(client, "default");
345+
/// let pp = PatchParams::default();
346+
///
347+
/// let patch = serde_json::json!({
348+
/// "spec": {
349+
/// "containers": [{
350+
/// "name": "mycontainer",
351+
/// "resources": {
352+
/// "requests": {
353+
/// "cpu": "200m",
354+
/// "memory": "512Mi"
355+
/// }
356+
/// }
357+
/// }]
358+
/// }
359+
/// });
360+
///
361+
/// pods.patch_resize("mypod", &pp, &Patch::Strategic(patch)).await?;
362+
/// # Ok(())
363+
/// # }
364+
/// ```
365+
pub async fn patch_resize<P: serde::Serialize>(
366+
&self,
367+
name: &str,
368+
pp: &PatchParams,
369+
patch: &Patch<P>,
370+
) -> Result<K> {
371+
let mut req = self
372+
.request
373+
.patch_subresource("resize", name, pp, patch)
374+
.map_err(Error::BuildRequest)?;
375+
req.extensions_mut().insert("patch_resize");
376+
self.client.request::<K>(req).await
377+
}
378+
379+
/// Replace the resize sub resource entirely.
380+
///
381+
/// This works similarly to [`Api::replace`] but uses the resize subresource.
382+
/// Takes a full Pod object with updated container resource requirements.
383+
///
384+
/// Note that only certain container resource fields can be modified. See the
385+
/// [limitations](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/#limitations)
386+
/// for details on what can be changed.
387+
///
388+
/// # Example
389+
///
390+
/// ```no_run
391+
/// use k8s_openapi::api::core::v1::Pod;
392+
/// use kube::{Api, api::PostParams};
393+
/// # async fn wrapper() -> Result<(), Box<dyn std::error::Error>> {
394+
/// # let client = kube::Client::try_default().await?;
395+
/// let pods: Api<Pod> = Api::namespaced(client, "default");
396+
/// let pp = PostParams::default();
397+
///
398+
/// // Get current pod
399+
/// let mut pod = pods.get("mypod").await?;
400+
///
401+
/// // Modify resource requirements
402+
/// if let Some(spec) = &mut pod.spec {
403+
/// if let Some(container) = spec.containers.get_mut(0) {
404+
/// if let Some(resources) = &mut container.resources {
405+
/// // Update CPU/memory limits or requests
406+
/// // ...
407+
/// }
408+
/// }
409+
/// }
410+
///
411+
/// pods.replace_resize("mypod", &pp, &pod).await?;
412+
/// # Ok(())
413+
/// # }
414+
/// ```
415+
pub async fn replace_resize(&self, name: &str, pp: &PostParams, data: &K) -> Result<K>
416+
where
417+
K: Serialize,
418+
{
419+
let mut req = self
420+
.request
421+
.replace_subresource(
422+
"resize",
423+
name,
424+
pp,
425+
serde_json::to_vec(data).map_err(Error::SerdeError)?,
426+
)
427+
.map_err(Error::BuildRequest)?;
428+
req.extensions_mut().insert("replace_resize");
429+
self.client.request::<K>(req).await
430+
}
431+
}
432+
}
433+
286434
// ----------------------------------------------------------------------------
287435

288436
// TODO: Replace examples with owned custom resources. Bad practice to write to owned objects
@@ -610,3 +758,19 @@ where
610758
Ok(Portforwarder::new(connection.into_stream(), ports))
611759
}
612760
}
761+
762+
// ----------------------------------------------------------------------------
763+
// Resize subresource tests
764+
// ----------------------------------------------------------------------------
765+
766+
#[test]
767+
fn resize_path() {
768+
use crate::api::{Request, Resource};
769+
use k8s_openapi::api::core::v1 as corev1;
770+
771+
k8s_openapi::k8s_if_ge_1_33! {
772+
let url = corev1::Pod::url_path(&(), Some("ns"));
773+
let req = Request::new(url).get_subresource("resize", "mypod").unwrap();
774+
assert_eq!(req.uri(), "/api/v1/namespaces/ns/pods/mypod/resize");
775+
}
776+
}

0 commit comments

Comments
 (0)