kube_client/discovery/
apigroup.rs

1use super::parse::{self, GroupVersionData};
2use crate::{error::DiscoveryError, Client, Error, Result};
3use k8s_openapi::apimachinery::pkg::apis::meta::v1::{APIGroup, APIVersions};
4pub use kube_core::discovery::{ApiCapabilities, ApiResource};
5use kube_core::{
6    gvk::{GroupVersion, GroupVersionKind, ParseGroupVersionError},
7    Version,
8};
9use std::{cmp::Reverse, collections::HashMap, iter::Iterator};
10
11/// Describes one API groups collected resources and capabilities.
12///
13/// Each `ApiGroup` contains all data pinned to a each version.
14/// In particular, one data set within the `ApiGroup` for `"apiregistration.k8s.io"`
15/// is the subset pinned to `"v1"`; commonly referred to as `"apiregistration.k8s.io/v1"`.
16///
17/// If you know the version of the discovered group, you can fetch it directly:
18/// ```no_run
19/// use kube::{Client, api::{Api, DynamicObject}, discovery, ResourceExt};
20/// #[tokio::main]
21/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
22///     let client = Client::try_default().await?;
23///     let apigroup = discovery::group(&client, "apiregistration.k8s.io").await?;
24///      for (apiresource, caps) in apigroup.versioned_resources("v1") {
25///          println!("Found ApiResource {}", apiresource.kind);
26///      }
27///     Ok(())
28/// }
29/// ```
30///
31/// But if you do not know this information, you can use [`ApiGroup::preferred_version_or_latest`].
32///
33/// Whichever way you choose the end result is something describing a resource and its abilities:
34/// - `Vec<(ApiResource, `ApiCapabilities)>` :: for all resources in a versioned ApiGroup
35/// - `(ApiResource, ApiCapabilities)` :: for a single kind under a versioned ApiGroud
36///
37/// These two types: [`ApiResource`], and [`ApiCapabilities`]
38/// should contain the information needed to construct an [`Api`](crate::Api) and start querying the kubernetes API.
39/// You will likely need to use [`DynamicObject`] as the generic type for Api to do this,
40/// as well as the [`ApiResource`] for the `DynamicType` for the [`Resource`] trait.
41///
42/// ```no_run
43/// use kube::{Client, api::{Api, DynamicObject}, discovery, ResourceExt};
44/// #[tokio::main]
45/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
46///     let client = Client::try_default().await?;
47///     let apigroup = discovery::group(&client, "apiregistration.k8s.io").await?;
48///     let (ar, caps) = apigroup.recommended_kind("APIService").unwrap();
49///     let api: Api<DynamicObject> = Api::all_with(client.clone(), &ar);
50///     for service in api.list(&Default::default()).await? {
51///         println!("Found APIService: {}", service.name_any());
52///     }
53///     Ok(())
54/// }
55/// ```
56///
57/// This type represents an abstraction over the native [`APIGroup`] to provide easier access to underlying group resources.
58///
59/// ### Common Pitfall
60/// Version preference and recommendations shown herein is a **group concept**, not a resource-wide concept.
61/// A common mistake is have different stored versions for resources within a group, and then receive confusing results from this module.
62/// Resources in a shared group should share versions - and transition together - to minimize confusion.
63/// See <https://kubernetes.io/docs/concepts/overview/kubernetes-api/#api-groups-and-versioning> for more info.
64///
65/// [`ApiResource`]: crate::discovery::ApiResource
66/// [`ApiCapabilities`]: crate::discovery::ApiCapabilities
67/// [`DynamicObject`]: crate::api::DynamicObject
68/// [`Resource`]: crate::Resource
69/// [`ApiGroup::preferred_version_or_latest`]: crate::discovery::ApiGroup::preferred_version_or_latest
70/// [`ApiGroup::versioned_resources`]: crate::discovery::ApiGroup::versioned_resources
71/// [`ApiGroup::recommended_resources`]: crate::discovery::ApiGroup::recommended_resources
72/// [`ApiGroup::recommended_kind`]: crate::discovery::ApiGroup::recommended_kind
73pub struct ApiGroup {
74    /// Name of the group e.g. apiregistration.k8s.io
75    name: String,
76    /// List of resource information, capabilities at particular versions
77    data: Vec<GroupVersionData>,
78    /// Preferred version if exported by the `APIGroup`
79    preferred: Option<String>,
80}
81
82/// Internal queriers to convert from an APIGroup (or APIVersions for core) to our ApiGroup
83///
84/// These queriers ignore groups with empty versions.
85/// This ensures that `ApiGroup::preferred_version_or_latest` always have an answer.
86/// On construction, they also sort the internal vec of GroupVersionData according to `Version`.
87impl ApiGroup {
88    pub(crate) async fn query_apis(client: &Client, g: APIGroup) -> Result<Self> {
89        tracing::debug!(name = g.name.as_str(), "Listing group versions");
90        let key = g.name;
91        if g.versions.is_empty() {
92            return Err(Error::Discovery(DiscoveryError::EmptyApiGroup(key)));
93        }
94        let mut data = vec![];
95        for vers in &g.versions {
96            let resources = client.list_api_group_resources(&vers.group_version).await?;
97            data.push(GroupVersionData::new(vers.version.clone(), resources)?);
98        }
99        let mut group = ApiGroup {
100            name: key,
101            data,
102            preferred: g.preferred_version.map(|v| v.version),
103        };
104        group.sort_versions();
105        Ok(group)
106    }
107
108    pub(crate) async fn query_core(client: &Client, coreapis: APIVersions) -> Result<Self> {
109        let mut data = vec![];
110        let key = ApiGroup::CORE_GROUP.to_string();
111        if coreapis.versions.is_empty() {
112            return Err(Error::Discovery(DiscoveryError::EmptyApiGroup(key)));
113        }
114        for v in coreapis.versions {
115            let resources = client.list_core_api_resources(&v).await?;
116            data.push(GroupVersionData::new(v, resources)?);
117        }
118        let mut group = ApiGroup {
119            name: ApiGroup::CORE_GROUP.to_string(),
120            data,
121            preferred: Some("v1".to_string()),
122        };
123        group.sort_versions();
124        Ok(group)
125    }
126
127    fn sort_versions(&mut self) {
128        self.data
129            .sort_by_cached_key(|gvd| Reverse(Version::parse(gvd.version.as_str()).priority()))
130    }
131
132    // shortcut method to give cheapest return for a single GVK
133    pub(crate) async fn query_gvk(
134        client: &Client,
135        gvk: &GroupVersionKind,
136    ) -> Result<(ApiResource, ApiCapabilities)> {
137        let apiver = gvk.api_version();
138        let list = if gvk.group.is_empty() {
139            client.list_core_api_resources(&apiver).await?
140        } else {
141            client.list_api_group_resources(&apiver).await?
142        };
143        for res in &list.resources {
144            if res.kind == gvk.kind && !res.name.contains('/') {
145                let ar = parse::parse_apiresource(res, &list.group_version).map_err(
146                    |ParseGroupVersionError(s)| Error::Discovery(DiscoveryError::InvalidGroupVersion(s)),
147                )?;
148                let caps = parse::parse_apicapabilities(&list, &res.name)?;
149                return Ok((ar, caps));
150            }
151        }
152        Err(Error::Discovery(DiscoveryError::MissingKind(format!("{gvk:?}"))))
153    }
154
155    // shortcut method to give cheapest return for a pinned group
156    pub(crate) async fn query_gv(client: &Client, gv: &GroupVersion) -> Result<Self> {
157        let apiver = gv.api_version();
158        let list = if gv.group.is_empty() {
159            client.list_core_api_resources(&apiver).await?
160        } else {
161            client.list_api_group_resources(&apiver).await?
162        };
163        let data = GroupVersionData::new(gv.version.clone(), list)?;
164        let group = ApiGroup {
165            name: gv.group.clone(),
166            data: vec![data],
167            preferred: Some(gv.version.clone()), // you preferred what you asked for
168        };
169        Ok(group)
170    }
171}
172
173/// Public ApiGroup interface
174impl ApiGroup {
175    /// Core group name
176    pub const CORE_GROUP: &'static str = "";
177
178    /// Returns the name of this group.
179    pub fn name(&self) -> &str {
180        &self.name
181    }
182
183    /// Returns served versions (e.g. `["v1", "v2beta1"]`) of this group.
184    ///
185    /// This [`Iterator`] is never empty, and returns elements in descending order of [`Version`](kube_core::Version):
186    /// - Stable versions (with the last being the first)
187    /// - Beta versions (with the last being the first)
188    /// - Alpha versions (with the last being the first)
189    /// - Other versions, alphabetically
190    pub fn versions(&self) -> impl Iterator<Item = &str> {
191        self.data.as_slice().iter().map(|gvd| gvd.version.as_str())
192    }
193
194    /// Returns preferred version for working with given group.
195    ///
196    /// Please note the [ApiGroup Common Pitfall](ApiGroup#common-pitfall).
197    pub fn preferred_version(&self) -> Option<&str> {
198        self.preferred.as_deref()
199    }
200
201    /// Returns the preferred version or latest version for working with given group.
202    ///
203    /// If the server does not recommend a version, we pick the "most stable and most recent" version
204    /// in accordance with [kubernetes version priority](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#version-priority)
205    /// via the descending sort order from [`Version`](kube_core::Version).
206    ///
207    /// Please note the [ApiGroup Common Pitfall](ApiGroup#common-pitfall).
208    pub fn preferred_version_or_latest(&self) -> &str {
209        // NB: self.versions is non-empty by construction in ApiGroup
210        self.preferred
211            .as_deref()
212            .unwrap_or_else(|| self.versions().next().unwrap())
213    }
214
215    /// Returns the resources in the group at an arbitrary version string.
216    ///
217    /// If the group does not support this version, the returned vector is empty.
218    ///
219    /// If you are looking for the api recommended list of resources, or just on particular kind
220    /// consider [`ApiGroup::recommended_resources`] or [`ApiGroup::recommended_kind`] instead.
221    pub fn versioned_resources(&self, ver: &str) -> Vec<(ApiResource, ApiCapabilities)> {
222        self.data
223            .iter()
224            .find(|gvd| gvd.version == ver)
225            .map(|gvd| gvd.resources.clone())
226            .unwrap_or_default()
227    }
228
229    /// Returns the recommended (preferred or latest) versioned resources in the group
230    ///
231    /// ```no_run
232    /// use kube::{Client, api::{Api, DynamicObject}, discovery::{self, verbs}, ResourceExt};
233    /// #[tokio::main]
234    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
235    ///     let client = Client::try_default().await?;
236    ///     let apigroup = discovery::group(&client, "apiregistration.k8s.io").await?;
237    ///     for (ar, caps) in apigroup.recommended_resources() {
238    ///         if !caps.supports_operation(verbs::LIST) {
239    ///             continue;
240    ///         }
241    ///         let api: Api<DynamicObject> = Api::all_with(client.clone(), &ar);
242    ///         for inst in api.list(&Default::default()).await? {
243    ///             println!("Found {}: {}", ar.kind, inst.name_any());
244    ///         }
245    ///     }
246    ///     Ok(())
247    /// }
248    /// ```
249    ///
250    /// This is equivalent to taking the [`ApiGroup::versioned_resources`] at the [`ApiGroup::preferred_version_or_latest`].
251    ///
252    /// Please note the [ApiGroup Common Pitfall](ApiGroup#common-pitfall).
253    pub fn recommended_resources(&self) -> Vec<(ApiResource, ApiCapabilities)> {
254        let ver = self.preferred_version_or_latest();
255        self.versioned_resources(ver)
256    }
257
258    ///  Returns all resources in the group at their the most stable respective version
259    ///
260    /// ```no_run
261    /// use kube::{Client, api::{Api, DynamicObject}, discovery::{self, verbs}, ResourceExt};
262    /// #[tokio::main]
263    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
264    ///     let client = Client::try_default().await?;
265    ///     let apigroup = discovery::group(&client, "apiregistration.k8s.io").await?;
266    ///     for (ar, caps) in apigroup.resources_by_stability() {
267    ///         if !caps.supports_operation(verbs::LIST) {
268    ///             continue;
269    ///         }
270    ///         let api: Api<DynamicObject> = Api::all_with(client.clone(), &ar);
271    ///         for inst in api.list(&Default::default()).await? {
272    ///             println!("Found {}: {}", ar.kind, inst.name_any());
273    ///         }
274    ///     }
275    ///     Ok(())
276    /// }
277    /// ```
278    /// See an example in [examples/kubectl.rs](https://github.com/kube-rs/kube/blob/main/examples/kubectl.rs)
279    pub fn resources_by_stability(&self) -> Vec<(ApiResource, ApiCapabilities)> {
280        let mut lookup = HashMap::new();
281        self.data.iter().for_each(|gvd| {
282            gvd.resources.iter().for_each(|resource| {
283                lookup
284                    .entry(resource.0.kind.clone())
285                    .or_insert_with(Vec::new)
286                    .push(resource);
287            })
288        });
289        lookup
290            .into_values()
291            .map(|mut v| {
292                v.sort_by_cached_key(|(ar, _)| Reverse(Version::parse(ar.version.as_str()).priority()));
293                v[0].to_owned()
294            })
295            .collect()
296    }
297
298    /// Returns the recommended version of the `kind` in the recommended resources (if found)
299    ///
300    /// ```no_run
301    /// use kube::{Client, api::{Api, DynamicObject}, discovery, ResourceExt};
302    /// #[tokio::main]
303    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
304    ///     let client = Client::try_default().await?;
305    ///     let apigroup = discovery::group(&client, "apiregistration.k8s.io").await?;
306    ///     let (ar, caps) = apigroup.recommended_kind("APIService").unwrap();
307    ///     let api: Api<DynamicObject> = Api::all_with(client.clone(), &ar);
308    ///     for service in api.list(&Default::default()).await? {
309    ///         println!("Found APIService: {}", service.name_any());
310    ///     }
311    ///     Ok(())
312    /// }
313    /// ```
314    ///
315    /// This is equivalent to filtering the [`ApiGroup::versioned_resources`] at [`ApiGroup::preferred_version_or_latest`] against a chosen `kind`.
316    pub fn recommended_kind(&self, kind: &str) -> Option<(ApiResource, ApiCapabilities)> {
317        let ver = self.preferred_version_or_latest();
318        for (ar, caps) in self.versioned_resources(ver) {
319            if ar.kind == kind {
320                return Some((ar, caps));
321            }
322        }
323        None
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    use kube_core::discovery::Scope;
331
332    #[test]
333    fn test_resources_by_stability() {
334        let ac = ApiCapabilities {
335            scope: Scope::Namespaced,
336            subresources: vec![],
337            operations: vec![],
338        };
339
340        let testlowversioncr_v1alpha1 = ApiResource {
341            group: String::from("kube.rs"),
342            version: String::from("v1alpha1"),
343            kind: String::from("TestLowVersionCr"),
344            api_version: String::from("kube.rs/v1alpha1"),
345            plural: String::from("testlowversioncrs"),
346        };
347
348        let testcr_v1 = ApiResource {
349            group: String::from("kube.rs"),
350            version: String::from("v1"),
351            kind: String::from("TestCr"),
352            api_version: String::from("kube.rs/v1"),
353            plural: String::from("testcrs"),
354        };
355
356        let testcr_v2alpha1 = ApiResource {
357            group: String::from("kube.rs"),
358            version: String::from("v2alpha1"),
359            kind: String::from("TestCr"),
360            api_version: String::from("kube.rs/v2alpha1"),
361            plural: String::from("testcrs"),
362        };
363
364        let group = ApiGroup {
365            name: "kube.rs".to_string(),
366            data: vec![
367                GroupVersionData {
368                    version: "v1alpha1".to_string(),
369                    resources: vec![(testlowversioncr_v1alpha1, ac.clone())],
370                },
371                GroupVersionData {
372                    version: "v1".to_string(),
373                    resources: vec![(testcr_v1, ac.clone())],
374                },
375                GroupVersionData {
376                    version: "v2alpha1".to_string(),
377                    resources: vec![(testcr_v2alpha1, ac)],
378                },
379            ],
380            preferred: Some(String::from("v1")),
381        };
382
383        let resources = group.resources_by_stability();
384        assert!(
385            resources
386                .iter()
387                .any(|(ar, _)| ar.kind == "TestCr" && ar.version == "v1"),
388            "wrong stable version"
389        );
390        assert!(
391            resources
392                .iter()
393                .any(|(ar, _)| ar.kind == "TestLowVersionCr" && ar.version == "v1alpha1"),
394            "lost low version resource"
395        );
396    }
397}