kube_core/
subresource.rs

1//! Request builder types and parameters for subresources
2use std::fmt::Debug;
3
4use crate::{
5    params::{DeleteParams, PostParams},
6    request::{Error, Request, JSON_MIME},
7};
8
9pub use k8s_openapi::api::autoscaling::v1::{Scale, ScaleSpec, ScaleStatus};
10
11// ----------------------------------------------------------------------------
12// Log subresource
13// ----------------------------------------------------------------------------
14
15/// Params for logging
16#[derive(Default, Clone, Debug)]
17pub struct LogParams {
18    /// The container for which to stream logs. Defaults to only container if there is one container in the pod.
19    pub container: Option<String>,
20    /// Follow the log stream of the pod. Defaults to `false`.
21    pub follow: bool,
22    /// If set, the number of bytes to read from the server before terminating the log output.
23    /// This may not display a complete final line of logging, and may return slightly more or slightly less than the specified limit.
24    pub limit_bytes: Option<i64>,
25    /// If `true`, then the output is pretty printed.
26    pub pretty: bool,
27    /// Return previous terminated container logs. Defaults to `false`.
28    pub previous: bool,
29    /// A relative time in seconds before the current time from which to show logs.
30    /// If this value precedes the time a pod was started, only logs since the pod start will be returned.
31    /// If this value is in the future, no logs will be returned. Only one of sinceSeconds or sinceTime may be specified.
32    pub since_seconds: Option<i64>,
33    /// An RFC3339 timestamp from which to show logs. If this value
34    /// precedes the time a pod was started, only logs since the pod start will be returned.
35    /// If this value is in the future, no logs will be returned.
36    /// Only one of sinceSeconds or sinceTime may be specified.
37    pub since_time: Option<chrono::DateTime<chrono::Utc>>,
38    /// If set, the number of lines from the end of the logs to show.
39    /// If not specified, logs are shown from the creation of the container or sinceSeconds or sinceTime
40    pub tail_lines: Option<i64>,
41    /// If `true`, add an RFC3339 or RFC3339Nano timestamp at the beginning of every line of log output. Defaults to `false`.
42    pub timestamps: bool,
43}
44
45impl Request {
46    /// Get a pod logs
47    pub fn logs(&self, name: &str, lp: &LogParams) -> Result<http::Request<Vec<u8>>, Error> {
48        let target = format!("{}/{}/log?", self.url_path, name);
49        let mut qp = form_urlencoded::Serializer::new(target);
50
51        if let Some(container) = &lp.container {
52            qp.append_pair("container", container);
53        }
54
55        if lp.follow {
56            qp.append_pair("follow", "true");
57        }
58
59        if let Some(lb) = &lp.limit_bytes {
60            qp.append_pair("limitBytes", &lb.to_string());
61        }
62
63        if lp.pretty {
64            qp.append_pair("pretty", "true");
65        }
66
67        if lp.previous {
68            qp.append_pair("previous", "true");
69        }
70
71        if let Some(ss) = &lp.since_seconds {
72            qp.append_pair("sinceSeconds", &ss.to_string());
73        } else if let Some(st) = &lp.since_time {
74            let ser_since = st.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
75            qp.append_pair("sinceTime", &ser_since);
76        }
77
78        if let Some(tl) = &lp.tail_lines {
79            qp.append_pair("tailLines", &tl.to_string());
80        }
81
82        if lp.timestamps {
83            qp.append_pair("timestamps", "true");
84        }
85
86        let urlstr = qp.finish();
87        let req = http::Request::get(urlstr);
88        req.body(vec![]).map_err(Error::BuildRequest)
89    }
90}
91
92// ----------------------------------------------------------------------------
93// Eviction subresource
94// ----------------------------------------------------------------------------
95
96/// Params for evictable objects
97#[derive(Default, Clone)]
98pub struct EvictParams {
99    /// How the eviction should occur
100    pub delete_options: Option<DeleteParams>,
101    /// How the http post should occur
102    pub post_options: PostParams,
103}
104
105impl Request {
106    /// Create an eviction
107    pub fn evict(&self, name: &str, ep: &EvictParams) -> Result<http::Request<Vec<u8>>, Error> {
108        let target = format!("{}/{}/eviction?", self.url_path, name);
109        // This is technically identical to Request::create, but different url
110        let pp = &ep.post_options;
111        pp.validate()?;
112        let mut qp = form_urlencoded::Serializer::new(target);
113        pp.populate_qp(&mut qp);
114        let urlstr = qp.finish();
115        // eviction body parameters are awkward, need metadata with name
116        let data = serde_json::to_vec(&serde_json::json!({
117            "delete_options": ep.delete_options,
118            "metadata": { "name": name }
119        }))
120        .map_err(Error::SerializeBody)?;
121        let req = http::Request::post(urlstr).header(http::header::CONTENT_TYPE, JSON_MIME);
122        req.body(data).map_err(Error::BuildRequest)
123    }
124}
125
126// ----------------------------------------------------------------------------
127// Attach subresource
128// ----------------------------------------------------------------------------
129/// Parameters for attaching to a container in a Pod.
130///
131/// - One of `stdin`, `stdout`, or `stderr` must be `true`.
132/// - `stderr` and `tty` cannot both be `true` because multiplexing is not supported with TTY.
133#[cfg(feature = "ws")]
134#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
135#[derive(Debug)]
136pub struct AttachParams {
137    /// The name of the container to attach.
138    /// Defaults to the only container if there is only one container in the pod.
139    pub container: Option<String>,
140    /// Attach to the container's standard input. Defaults to `false`.
141    ///
142    /// Call [`AttachedProcess::stdin`](https://docs.rs/kube/*/kube/api/struct.AttachedProcess.html#method.stdin) to obtain a writer.
143    pub stdin: bool,
144    /// Attach to the container's standard output. Defaults to `true`.
145    ///
146    /// Call [`AttachedProcess::stdout`](https://docs.rs/kube/*/kube/api/struct.AttachedProcess.html#method.stdout) to obtain a reader.
147    pub stdout: bool,
148    /// Attach to the container's standard error. Defaults to `true`.
149    ///
150    /// Call [`AttachedProcess::stderr`](https://docs.rs/kube/*/kube/api/struct.AttachedProcess.html#method.stderr) to obtain a reader.
151    pub stderr: bool,
152    /// Allocate TTY. Defaults to `false`.
153    ///
154    /// NOTE: Terminal resizing is not implemented yet.
155    pub tty: bool,
156
157    /// The maximum amount of bytes that can be written to the internal `stdin`
158    /// pipe before the write returns `Poll::Pending`.
159    /// Defaults to 1024.
160    ///
161    /// This is not sent to the server.
162    pub max_stdin_buf_size: Option<usize>,
163    /// The maximum amount of bytes that can be written to the internal `stdout`
164    /// pipe before the write returns `Poll::Pending`.
165    /// Defaults to 1024.
166    ///
167    /// This is not sent to the server.
168    pub max_stdout_buf_size: Option<usize>,
169    /// The maximum amount of bytes that can be written to the internal `stderr`
170    /// pipe before the write returns `Poll::Pending`.
171    /// Defaults to 1024.
172    ///
173    /// This is not sent to the server.
174    pub max_stderr_buf_size: Option<usize>,
175}
176
177#[cfg(feature = "ws")]
178#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
179impl Default for AttachParams {
180    // Default matching the server's defaults.
181    fn default() -> Self {
182        Self {
183            container: None,
184            stdin: false,
185            stdout: true,
186            stderr: true,
187            tty: false,
188            max_stdin_buf_size: None,
189            max_stdout_buf_size: None,
190            max_stderr_buf_size: None,
191        }
192    }
193}
194
195#[cfg(feature = "ws")]
196#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
197impl AttachParams {
198    /// Default parameters for an tty exec with stdin and stdout
199    #[must_use]
200    pub fn interactive_tty() -> Self {
201        Self {
202            stdin: true,
203            stdout: true,
204            stderr: false,
205            tty: true,
206            ..Default::default()
207        }
208    }
209
210    /// Specify the container to execute in.
211    #[must_use]
212    pub fn container<T: Into<String>>(mut self, container: T) -> Self {
213        self.container = Some(container.into());
214        self
215    }
216
217    /// Set `stdin` field.
218    #[must_use]
219    pub fn stdin(mut self, enable: bool) -> Self {
220        self.stdin = enable;
221        self
222    }
223
224    /// Set `stdout` field.
225    #[must_use]
226    pub fn stdout(mut self, enable: bool) -> Self {
227        self.stdout = enable;
228        self
229    }
230
231    /// Set `stderr` field.
232    #[must_use]
233    pub fn stderr(mut self, enable: bool) -> Self {
234        self.stderr = enable;
235        self
236    }
237
238    /// Set `tty` field.
239    #[must_use]
240    pub fn tty(mut self, enable: bool) -> Self {
241        self.tty = enable;
242        self
243    }
244
245    /// Set `max_stdin_buf_size` field.
246    #[must_use]
247    pub fn max_stdin_buf_size(mut self, size: usize) -> Self {
248        self.max_stdin_buf_size = Some(size);
249        self
250    }
251
252    /// Set `max_stdout_buf_size` field.
253    #[must_use]
254    pub fn max_stdout_buf_size(mut self, size: usize) -> Self {
255        self.max_stdout_buf_size = Some(size);
256        self
257    }
258
259    /// Set `max_stderr_buf_size` field.
260    #[must_use]
261    pub fn max_stderr_buf_size(mut self, size: usize) -> Self {
262        self.max_stderr_buf_size = Some(size);
263        self
264    }
265
266    pub(crate) fn validate(&self) -> Result<(), Error> {
267        if !self.stdin && !self.stdout && !self.stderr {
268            return Err(Error::Validation(
269                "AttachParams: one of stdin, stdout, or stderr must be true".into(),
270            ));
271        }
272
273        if self.stderr && self.tty {
274            // Multiplexing is not supported with TTY
275            return Err(Error::Validation(
276                "AttachParams: tty and stderr cannot both be true".into(),
277            ));
278        }
279
280        Ok(())
281    }
282
283    fn append_to_url_serializer(&self, qp: &mut form_urlencoded::Serializer<String>) {
284        if self.stdin {
285            qp.append_pair("stdin", "true");
286        }
287        if self.stdout {
288            qp.append_pair("stdout", "true");
289        }
290        if self.stderr {
291            qp.append_pair("stderr", "true");
292        }
293        if self.tty {
294            qp.append_pair("tty", "true");
295        }
296        if let Some(container) = &self.container {
297            qp.append_pair("container", container);
298        }
299    }
300
301    #[cfg(feature = "kubelet-debug")]
302    // https://github.com/kubernetes/kubernetes/blob/466d9378dbb0a185df9680657f5cd96d5e5aab57/pkg/apis/core/types.go#L6005-L6013
303    pub(crate) fn append_to_url_serializer_local(&self, qp: &mut form_urlencoded::Serializer<String>) {
304        if self.stdin {
305            qp.append_pair("input", "1");
306        }
307        if self.stdout {
308            qp.append_pair("output", "1");
309        }
310        if self.stderr {
311            qp.append_pair("error", "1");
312        }
313        if self.tty {
314            qp.append_pair("tty", "1");
315        }
316    }
317}
318
319#[cfg(feature = "ws")]
320#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
321impl Request {
322    /// Attach to a pod
323    pub fn attach(&self, name: &str, ap: &AttachParams) -> Result<http::Request<Vec<u8>>, Error> {
324        ap.validate()?;
325
326        let target = format!("{}/{}/attach?", self.url_path, name);
327        let mut qp = form_urlencoded::Serializer::new(target);
328        ap.append_to_url_serializer(&mut qp);
329
330        let req = http::Request::get(qp.finish());
331        req.body(vec![]).map_err(Error::BuildRequest)
332    }
333}
334
335// ----------------------------------------------------------------------------
336// Exec subresource
337// ----------------------------------------------------------------------------
338#[cfg(feature = "ws")]
339#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
340impl Request {
341    /// Execute command in a pod
342    pub fn exec<I, T>(
343        &self,
344        name: &str,
345        command: I,
346        ap: &AttachParams,
347    ) -> Result<http::Request<Vec<u8>>, Error>
348    where
349        I: IntoIterator<Item = T>,
350        T: Into<String>,
351    {
352        ap.validate()?;
353
354        let target = format!("{}/{}/exec?", self.url_path, name);
355        let mut qp = form_urlencoded::Serializer::new(target);
356        ap.append_to_url_serializer(&mut qp);
357
358        for c in command.into_iter() {
359            qp.append_pair("command", &c.into());
360        }
361
362        let req = http::Request::get(qp.finish());
363        req.body(vec![]).map_err(Error::BuildRequest)
364    }
365}
366
367// ----------------------------------------------------------------------------
368// Portforward subresource
369// ----------------------------------------------------------------------------
370#[cfg(feature = "ws")]
371#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
372impl Request {
373    /// Request to forward ports of a pod
374    pub fn portforward(&self, name: &str, ports: &[u16]) -> Result<http::Request<Vec<u8>>, Error> {
375        if ports.is_empty() {
376            return Err(Error::Validation("ports cannot be empty".into()));
377        }
378        if ports.len() > 128 {
379            return Err(Error::Validation(
380                "the number of ports cannot be more than 128".into(),
381            ));
382        }
383
384        if ports.len() > 1 {
385            let mut seen = std::collections::HashSet::with_capacity(ports.len());
386            for port in ports.iter() {
387                if seen.contains(port) {
388                    return Err(Error::Validation(format!(
389                        "ports must be unique, found multiple {port}"
390                    )));
391                }
392                seen.insert(port);
393            }
394        }
395
396        let base_url = format!("{}/{}/portforward?", self.url_path, name);
397        let mut qp = form_urlencoded::Serializer::new(base_url);
398        qp.append_pair(
399            "ports",
400            &ports.iter().map(|p| p.to_string()).collect::<Vec<_>>().join(","),
401        );
402
403        let req = http::Request::get(qp.finish());
404        req.body(vec![]).map_err(Error::BuildRequest)
405    }
406}
407
408// ----------------------------------------------------------------------------
409// tests
410// ----------------------------------------------------------------------------
411
412/// Cheap sanity check to ensure type maps work as expected
413#[cfg(test)]
414mod test {
415    use crate::{request::Request, resource::Resource};
416    use chrono::{DateTime, TimeZone, Utc};
417    use k8s::core::v1 as corev1;
418    use k8s_openapi::api as k8s;
419
420    use crate::subresource::LogParams;
421
422    #[test]
423    fn logs_all_params() {
424        let url = corev1::Pod::url_path(&(), Some("ns"));
425        let lp = LogParams {
426            container: Some("nginx".into()),
427            follow: true,
428            limit_bytes: Some(10 * 1024 * 1024),
429            pretty: true,
430            previous: true,
431            since_seconds: Some(3600),
432            since_time: None,
433            tail_lines: Some(4096),
434            timestamps: true,
435        };
436        let req = Request::new(url).logs("mypod", &lp).unwrap();
437        assert_eq!(req.uri(), "/api/v1/namespaces/ns/pods/mypod/log?&container=nginx&follow=true&limitBytes=10485760&pretty=true&previous=true&sinceSeconds=3600&tailLines=4096&timestamps=true");
438    }
439
440    #[test]
441    fn logs_since_time() {
442        let url = corev1::Pod::url_path(&(), Some("ns"));
443        let date: DateTime<Utc> = Utc.with_ymd_and_hms(2023, 10, 19, 13, 14, 26).unwrap();
444        let lp = LogParams {
445            since_seconds: None,
446            since_time: Some(date),
447            ..Default::default()
448        };
449        let req = Request::new(url).logs("mypod", &lp).unwrap();
450        assert_eq!(
451            req.uri(),
452            "/api/v1/namespaces/ns/pods/mypod/log?&sinceTime=2023-10-19T13%3A14%3A26Z" // cross-referenced with kubectl
453        );
454    }
455}