kube_client/client/
client_ext.rs

1use crate::{Client, Error, Result};
2use k8s_openapi::{
3    api::core::v1::{LocalObjectReference, Namespace as k8sNs, ObjectReference},
4    apimachinery::pkg::apis::meta::v1::OwnerReference,
5};
6use kube_core::{
7    object::ObjectList,
8    params::{GetParams, ListParams},
9    request::Request,
10    ApiResource, ClusterResourceScope, DynamicResourceScope, NamespaceResourceScope, Resource,
11};
12use serde::{de::DeserializeOwned, Serialize};
13use std::fmt::Debug;
14
15/// A marker trait to indicate cluster-wide operations are available
16trait ClusterScope {}
17/// A marker trait to indicate namespace-scoped operations are available
18trait NamespaceScope {}
19
20// k8s_openapi scopes get implementations for free
21impl ClusterScope for ClusterResourceScope {}
22impl NamespaceScope for NamespaceResourceScope {}
23// our DynamicResourceScope can masquerade as either
24impl NamespaceScope for DynamicResourceScope {}
25impl ClusterScope for DynamicResourceScope {}
26
27/// How to get the url for a collection
28///
29/// Pick one of `kube::client::Cluster` or `kube::client::Namespace`.
30pub trait CollectionUrl<K> {
31    fn url_path(&self) -> String;
32}
33
34/// How to get the url for an object
35///
36/// Pick one of `kube::client::Cluster` or `kube::client::Namespace`.
37pub trait ObjectUrl<K> {
38    fn url_path(&self) -> String;
39}
40
41/// Marker type for cluster level queries
42#[derive(Debug, Clone)]
43pub struct Cluster;
44/// Namespace newtype for namespace level queries
45///
46/// You can create this directly, or convert `From` a `String` / `&str`, or `TryFrom` an `k8s_openapi::api::core::v1::Namespace`
47#[derive(Debug, Clone)]
48pub struct Namespace(String);
49
50/// Referenced object name resolution
51pub trait ObjectRef<K>: ObjectUrl<K> {
52    fn name(&self) -> Option<&str>;
53}
54
55/// Reference resolver for a specified namespace
56pub trait NamespacedRef<K> {
57    /// Resolve reference in the provided namespace
58    fn within(&self, namespace: impl Into<Option<String>>) -> impl ObjectRef<K>;
59}
60
61impl<K> ObjectUrl<K> for ObjectReference
62where
63    K: Resource,
64{
65    fn url_path(&self) -> String {
66        url_path(
67            &ApiResource::from_gvk(&self.clone().into()),
68            self.namespace.clone(),
69        )
70    }
71}
72
73impl<K> ObjectRef<K> for ObjectReference
74where
75    K: Resource,
76{
77    fn name(&self) -> Option<&str> {
78        self.name.as_deref()
79    }
80}
81
82impl<K> NamespacedRef<K> for ObjectReference
83where
84    K: Resource,
85    K::Scope: NamespaceScope,
86{
87    fn within(&self, namespace: impl Into<Option<String>>) -> impl ObjectRef<K> {
88        Self {
89            namespace: namespace.into(),
90            ..self.clone()
91        }
92    }
93}
94
95impl<K> ObjectUrl<K> for OwnerReference
96where
97    K: Resource,
98    K::Scope: ClusterScope,
99{
100    fn url_path(&self) -> String {
101        url_path(&ApiResource::from_gvk(&self.clone().into()), None)
102    }
103}
104
105impl<K> ObjectRef<K> for OwnerReference
106where
107    K: Resource,
108    K::Scope: ClusterScope,
109{
110    fn name(&self) -> Option<&str> {
111        self.name.as_str().into()
112    }
113}
114
115impl<K> NamespacedRef<K> for OwnerReference
116where
117    K: Resource,
118    K::Scope: NamespaceScope,
119{
120    fn within(&self, namespace: impl Into<Option<String>>) -> impl ObjectRef<K> {
121        ObjectReference {
122            api_version: self.api_version.clone().into(),
123            namespace: namespace.into(),
124            name: self.name.clone().into(),
125            uid: self.uid.clone().into(),
126            kind: self.kind.clone().into(),
127            ..Default::default()
128        }
129    }
130}
131
132impl<K> NamespacedRef<K> for LocalObjectReference
133where
134    K: Resource,
135    K::DynamicType: Default,
136    K::Scope: NamespaceScope,
137{
138    fn within(&self, namespace: impl Into<Option<String>>) -> impl ObjectRef<K> {
139        let dt = Default::default();
140        ObjectReference {
141            api_version: K::api_version(&dt).to_string().into(),
142            namespace: namespace.into(),
143            name: Some(self.name.clone()),
144            kind: K::kind(&dt).to_string().into(),
145            ..Default::default()
146        }
147    }
148}
149
150/// Scopes for `unstable-client` [`Client#impl-Client`] extension methods
151pub mod scope {
152    pub use super::{Cluster, Namespace, NamespacedRef};
153}
154
155// All objects can be listed cluster-wide
156impl<K> CollectionUrl<K> for Cluster
157where
158    K: Resource,
159    K::DynamicType: Default,
160{
161    fn url_path(&self) -> String {
162        K::url_path(&K::DynamicType::default(), None)
163    }
164}
165
166// Only cluster-scoped objects can be named globally
167impl<K> ObjectUrl<K> for Cluster
168where
169    K: Resource,
170    K::DynamicType: Default,
171    K::Scope: ClusterScope,
172{
173    fn url_path(&self) -> String {
174        K::url_path(&K::DynamicType::default(), None)
175    }
176}
177
178// Only namespaced objects can be accessed via namespace
179impl<K> CollectionUrl<K> for Namespace
180where
181    K: Resource,
182    K::DynamicType: Default,
183    K::Scope: NamespaceScope,
184{
185    fn url_path(&self) -> String {
186        K::url_path(&K::DynamicType::default(), Some(&self.0))
187    }
188}
189
190impl<K> ObjectUrl<K> for Namespace
191where
192    K: Resource,
193    K::DynamicType: Default,
194    K::Scope: NamespaceScope,
195{
196    fn url_path(&self) -> String {
197        K::url_path(&K::DynamicType::default(), Some(&self.0))
198    }
199}
200
201// can be created from a complete native object
202impl TryFrom<&k8sNs> for Namespace {
203    type Error = NamespaceError;
204
205    fn try_from(ns: &k8sNs) -> Result<Namespace, Self::Error> {
206        if let Some(n) = &ns.meta().name {
207            Ok(Namespace(n.to_owned()))
208        } else {
209            Err(NamespaceError::MissingName)
210        }
211    }
212}
213// and from literals + owned strings
214impl From<&str> for Namespace {
215    fn from(ns: &str) -> Namespace {
216        Namespace(ns.to_owned())
217    }
218}
219impl From<String> for Namespace {
220    fn from(ns: String) -> Namespace {
221        Namespace(ns)
222    }
223}
224
225#[derive(thiserror::Error, Debug)]
226/// Failures to infer a namespace
227pub enum NamespaceError {
228    /// MissingName
229    #[error("Missing Namespace Name")]
230    MissingName,
231}
232
233/// Generic client extensions for the `unstable-client` feature
234///
235/// These methods allow users to query across a wide-array of resources without needing
236/// to explicitly create an [`Api`](crate::Api) for each one of them.
237///
238/// ## Usage
239/// 1. Create a [`Client`]
240/// 2. Specify the [`scope`] you are querying at via [`Cluster`] or [`Namespace`] as args
241/// 3. Specify the resource type you are using for serialization (e.g. a top level k8s-openapi type)
242///
243/// ## Example
244///
245/// ```no_run
246/// # use k8s_openapi::api::core::v1::{Pod, Service};
247/// # use kube::client::scope::{Namespace, Cluster};
248/// # use kube::prelude::*;
249/// # use kube::api::ListParams;
250/// # async fn wrapper() -> Result<(), Box<dyn std::error::Error>> {
251/// # let client: kube::Client = todo!();
252/// let lp = ListParams::default();
253/// // List at Cluster level for Pod resource:
254/// for pod in client.list::<Pod>(&lp, &Cluster).await? {
255///     println!("Found pod {} in {}", pod.name_any(), pod.namespace().unwrap());
256/// }
257/// // Namespaced Get for Service resource:
258/// let svc = client.get::<Service>("kubernetes", &Namespace::from("default")).await?;
259/// assert_eq!(svc.name_unchecked(), "kubernetes");
260/// # Ok(())
261/// # }
262/// ```
263impl Client {
264    /// Get a single instance of a `Resource` implementing type `K` at the specified scope.
265    ///
266    /// ```no_run
267    /// # use k8s_openapi::api::rbac::v1::ClusterRole;
268    /// # use k8s_openapi::api::core::v1::Service;
269    /// # use kube::client::scope::{Namespace, Cluster};
270    /// # use kube::prelude::*;
271    /// # use kube::api::GetParams;
272    /// # async fn wrapper() -> Result<(), Box<dyn std::error::Error>> {
273    /// # let client: kube::Client = todo!();
274    /// let cr = client.get::<ClusterRole>("cluster-admin", &Cluster).await?;
275    /// assert_eq!(cr.name_unchecked(), "cluster-admin");
276    /// let svc = client.get::<Service>("kubernetes", &Namespace::from("default")).await?;
277    /// assert_eq!(svc.name_unchecked(), "kubernetes");
278    /// # Ok(())
279    /// # }
280    /// ```
281    pub async fn get<K>(&self, name: &str, scope: &impl ObjectUrl<K>) -> Result<K>
282    where
283        K: Resource + Serialize + DeserializeOwned + Clone + Debug,
284        <K as Resource>::DynamicType: Default,
285    {
286        let mut req = Request::new(scope.url_path())
287            .get(name, &GetParams::default())
288            .map_err(Error::BuildRequest)?;
289        req.extensions_mut().insert("get");
290        self.request::<K>(req).await
291    }
292
293    /// Fetch a single instance of a `Resource` from a provided object reference.
294    ///
295    /// ```no_run
296    /// # use k8s_openapi::api::rbac::v1::ClusterRole;
297    /// # use k8s_openapi::apimachinery::pkg::apis::meta::v1::OwnerReference;
298    /// # use k8s_openapi::api::core::v1::{ObjectReference, LocalObjectReference};
299    /// # use k8s_openapi::api::core::v1::{Node, Pod, Service, Secret};
300    /// # use kube::client::scope::NamespacedRef;
301    /// # use kube::api::GetParams;
302    /// # use kube::prelude::*;
303    /// # use kube::api::DynamicObject;
304    /// # async fn wrapper() -> Result<(), Box<dyn std::error::Error>> {
305    /// # let client: kube::Client = todo!();
306    /// // cluster scoped
307    /// let cr: ClusterRole = todo!();
308    /// let cr: ClusterRole = client.fetch(&cr.object_ref(&())).await?;
309    /// assert_eq!(cr.name_unchecked(), "cluster-admin");
310    /// // namespace scoped
311    /// let svc: Service = todo!();
312    /// let svc: Service = client.fetch(&svc.object_ref(&())).await?;
313    /// assert_eq!(svc.name_unchecked(), "kubernetes");
314    /// // Fetch an owner of the resource
315    /// let pod: Pod = todo!();
316    /// let owner = pod
317    ///     .owner_references()
318    ///     .to_vec()
319    ///     .into_iter()
320    ///     .find(|r| r.kind == Node::kind(&()))
321    ///     .ok_or("Not Found")?;
322    /// let node: Node = client.fetch(&owner).await?;
323    /// // Namespace scoped objects require namespace
324    /// let pod: Pod = client.fetch(&owner.within("ns".to_string())).await?;
325    /// // Fetch dynamic object to resolve type later
326    /// let dynamic: DynamicObject = client.fetch(&owner.within("ns".to_string())).await?;
327    /// // Fetch using local object reference
328    /// let secret_ref = pod
329    ///     .spec
330    ///     .unwrap_or_default()
331    ///     .image_pull_secrets
332    ///     .unwrap_or_default()
333    ///     .get(0)
334    ///     .unwrap_or(&LocalObjectReference{name: "pull_secret".into()});
335    /// let secret: Secret = client.fetch(&secret_ref.within(pod.namespace())).await?;
336    /// # Ok(())
337    /// # }
338    /// ```
339    pub async fn fetch<K>(&self, reference: &impl ObjectRef<K>) -> Result<K>
340    where
341        K: Resource + Serialize + DeserializeOwned + Clone + Debug,
342    {
343        let mut req = Request::new(reference.url_path())
344            .get(
345                reference
346                    .name()
347                    .ok_or(Error::RefResolve("Reference is empty".to_string()))?,
348                &GetParams::default(),
349            )
350            .map_err(Error::BuildRequest)?;
351        req.extensions_mut().insert("get");
352        self.request::<K>(req).await
353    }
354
355    /// List instances of a `Resource` implementing type `K` at the specified scope.
356    ///
357    /// ```no_run
358    /// # use k8s_openapi::api::core::v1::Pod;
359    /// # use k8s_openapi::api::core::v1::Service;
360    /// # use kube::client::scope::{Namespace, Cluster};
361    /// # use kube::prelude::*;
362    /// # use kube::api::ListParams;
363    /// # async fn wrapper() -> Result<(), Box<dyn std::error::Error>> {
364    /// # let client: kube::Client = todo!();
365    /// let lp = ListParams::default();
366    /// for pod in client.list::<Pod>(&lp, &Cluster).await? {
367    ///     println!("Found pod {} in {}", pod.name_any(), pod.namespace().unwrap());
368    /// }
369    /// for svc in client.list::<Service>(&lp, &Namespace::from("default")).await? {
370    ///     println!("Found service {}", svc.name_any());
371    /// }
372    /// # Ok(())
373    /// # }
374    /// ```
375    pub async fn list<K>(&self, lp: &ListParams, scope: &impl CollectionUrl<K>) -> Result<ObjectList<K>>
376    where
377        K: Resource + Serialize + DeserializeOwned + Clone + Debug,
378        <K as Resource>::DynamicType: Default,
379    {
380        let mut req = Request::new(scope.url_path())
381            .list(lp)
382            .map_err(Error::BuildRequest)?;
383        req.extensions_mut().insert("list");
384        self.request::<ObjectList<K>>(req).await
385    }
386}
387
388// Resource url_path resolver
389fn url_path(r: &ApiResource, namespace: Option<String>) -> String {
390    let n = if let Some(ns) = namespace {
391        format!("namespaces/{ns}/")
392    } else {
393        "".into()
394    };
395    format!(
396        "/{group}/{api_version}/{namespaces}{plural}",
397        group = if r.group.is_empty() { "api" } else { "apis" },
398        api_version = r.api_version,
399        namespaces = n,
400        plural = r.plural
401    )
402}
403
404#[cfg(test)]
405mod test {
406    use crate::{
407        client::{
408            client_ext::NamespacedRef as _,
409            scope::{Cluster, Namespace},
410        },
411        Client,
412    };
413
414    use super::ListParams;
415    use k8s_openapi::api::core::v1::LocalObjectReference;
416    use kube_core::{DynamicObject, Resource as _, ResourceExt as _};
417
418    #[tokio::test]
419    #[ignore = "needs cluster (will list/get namespaces, pods, jobs, svcs, clusterroles)"]
420    async fn client_ext_list_get_pods_svcs() -> Result<(), Box<dyn std::error::Error>> {
421        use k8s_openapi::api::{
422            batch::v1::Job,
423            core::v1::{Namespace as k8sNs, Pod, Service},
424            rbac::v1::ClusterRole,
425        };
426
427        let client = Client::try_default().await?;
428        let lp = ListParams::default();
429        // cluster-scoped list
430        for ns in client.list::<k8sNs>(&lp, &Cluster).await? {
431            // namespaced list
432            for p in client.list::<Pod>(&lp, &Namespace::try_from(&ns)?).await? {
433                println!("Found pod {} in {}", p.name_any(), ns.name_any());
434            }
435        }
436        // across-namespace list
437        for j in client.list::<Job>(&lp, &Cluster).await? {
438            println!("Found job {} in {}", j.name_any(), j.namespace().unwrap());
439        }
440        // namespaced get
441        let default: Namespace = "default".into();
442        let svc = client.get::<Service>("kubernetes", &default).await?;
443        assert_eq!(svc.name_unchecked(), "kubernetes");
444        // global get
445        let ca = client.get::<ClusterRole>("cluster-admin", &Cluster).await?;
446        assert_eq!(ca.name_unchecked(), "cluster-admin");
447
448        Ok(())
449    }
450
451    #[tokio::test]
452    #[ignore = "needs cluster (will get svcs, clusterroles, pods, nodes)"]
453    async fn client_ext_fetch_ref_pods_svcs() -> Result<(), Box<dyn std::error::Error>> {
454        use k8s_openapi::api::{
455            core::v1::{Node, ObjectReference, Pod, Service},
456            rbac::v1::ClusterRole,
457        };
458
459        let client = Client::try_default().await?;
460        // namespaced fetch
461        let svc: Service = client
462            .fetch(&ObjectReference {
463                kind: Some(Service::kind(&()).into()),
464                api_version: Some(Service::api_version(&()).into()),
465                name: Some("kubernetes".into()),
466                namespace: Some("default".into()),
467                ..Default::default()
468            })
469            .await?;
470        assert_eq!(svc.name_unchecked(), "kubernetes");
471        // global fetch
472        let ca: ClusterRole = client
473            .fetch(&ObjectReference {
474                kind: Some(ClusterRole::kind(&()).into()),
475                api_version: Some(ClusterRole::api_version(&()).into()),
476                name: Some("cluster-admin".into()),
477                ..Default::default()
478            })
479            .await?;
480        assert_eq!(ca.name_unchecked(), "cluster-admin");
481        // namespaced fetch untyped
482        let svc: DynamicObject = client.fetch(&svc.object_ref(&())).await?;
483        assert_eq!(svc.name_unchecked(), "kubernetes");
484        // global fetch untyped
485        let ca: DynamicObject = client.fetch(&ca.object_ref(&())).await?;
486        assert_eq!(ca.name_unchecked(), "cluster-admin");
487
488        // Fetch using local object reference
489        let svc: Service = client
490            .fetch(
491                &LocalObjectReference {
492                    name: svc.name_any().into(),
493                }
494                .within(svc.namespace()),
495            )
496            .await?;
497        assert_eq!(svc.name_unchecked(), "kubernetes");
498
499        let kube_system: Namespace = "kube-system".into();
500        for pod in client
501            .list::<Pod>(
502                &ListParams::default().labels("component=kube-apiserver"),
503                &kube_system,
504            )
505            .await?
506        {
507            let owner = pod
508                .owner_references()
509                .iter()
510                .find(|r| r.kind == Node::kind(&()))
511                .ok_or("Not found")?;
512            let _: Node = client.fetch(owner).await?;
513        }
514
515        Ok(())
516    }
517
518    #[tokio::test]
519    #[ignore = "needs cluster (will get svcs, clusterroles, pods, nodes)"]
520    async fn fetch_fails() -> Result<(), Box<dyn std::error::Error>> {
521        use crate::error::Error;
522        use k8s_openapi::api::core::v1::{ObjectReference, Pod, Service};
523
524        let client = Client::try_default().await?;
525        // namespaced fetch
526        let svc: Service = client
527            .fetch(&ObjectReference {
528                kind: Some(Service::kind(&()).into()),
529                api_version: Some(Service::api_version(&()).into()),
530                name: Some("kubernetes".into()),
531                namespace: Some("default".into()),
532                ..Default::default()
533            })
534            .await?;
535        let err = client.fetch::<Pod>(&svc.object_ref(&())).await.unwrap_err();
536        assert!(matches!(err, Error::SerdeError(_)));
537        assert_eq!(err.to_string(), "Error deserializing response: invalid value: string \"Service\", expected Pod at line 1 column 17".to_string());
538
539        let obj: DynamicObject = client.fetch(&svc.object_ref(&())).await?;
540        let err = obj.try_parse::<Pod>().unwrap_err();
541        assert_eq!(err.to_string(), "failed to parse this DynamicObject into a Resource: invalid value: string \"Service\", expected Pod".to_string());
542
543        Ok(())
544    }
545}