Skip to main content

kube_client/discovery/
mod.rs

1//! High-level utilities for runtime API discovery.
2
3use crate::{Client, Result};
4pub use kube_core::discovery::{ApiCapabilities, ApiResource, Scope, verbs};
5use kube_core::gvk::GroupVersionKind;
6use std::collections::HashMap;
7mod apigroup;
8pub mod oneshot;
9pub use apigroup::ApiGroup;
10mod parse;
11
12// re-export one-shots
13pub use oneshot::{group, pinned_group, pinned_kind};
14
15/// How the Discovery client decides what api groups to scan
16enum DiscoveryMode {
17    /// Only allow explicitly listed apigroups
18    Allow(Vec<String>),
19    /// Allow all apigroups except the ones listed
20    Block(Vec<String>),
21}
22
23impl DiscoveryMode {
24    fn is_queryable(&self, group: &String) -> bool {
25        match &self {
26            Self::Allow(allowed) => allowed.contains(group),
27            Self::Block(blocked) => !blocked.contains(group),
28        }
29    }
30}
31
32/// A caching client for running API discovery against the Kubernetes API.
33///
34/// This simplifies the required querying and type matching, and stores the responses
35/// for each discovered api group and exposes helpers to access them.
36///
37/// The discovery process varies in complexity depending on:
38/// - how much you know about the kind(s) and group(s) you are interested in
39/// - how many groups you are interested in
40///
41/// Discovery can be performed on:
42/// - all api groups (default)
43/// - a subset of api groups (by setting Discovery::filter)
44///
45/// To make use of discovered apis, extract one or more [`ApiGroup`]s from it,
46/// or resolve a precise one using [`Discovery::resolve_gvk`](crate::discovery::Discovery::resolve_gvk).
47///
48/// If caching of results is __not required__, then a simpler [`oneshot`](crate::discovery::oneshot) discovery system can be used.
49///
50/// [`ApiGroup`]: crate::discovery::ApiGroup
51#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
52pub struct Discovery {
53    client: Client,
54    groups: HashMap<String, ApiGroup>,
55    mode: DiscoveryMode,
56}
57
58/// Caching discovery interface
59///
60/// Builds an internal map of its cache
61impl Discovery {
62    /// Construct a caching api discovery client
63    #[must_use]
64    pub fn new(client: Client) -> Self {
65        let groups = HashMap::new();
66        let mode = DiscoveryMode::Block(vec![]);
67        Self { client, groups, mode }
68    }
69
70    /// Configure the discovery client to only look for the listed apigroups
71    #[must_use]
72    pub fn filter(mut self, allow: &[&str]) -> Self {
73        self.mode = DiscoveryMode::Allow(allow.iter().map(ToString::to_string).collect());
74        self
75    }
76
77    /// Configure the discovery client to look for all apigroups except the listed ones
78    #[must_use]
79    pub fn exclude(mut self, deny: &[&str]) -> Self {
80        self.mode = DiscoveryMode::Block(deny.iter().map(ToString::to_string).collect());
81        self
82    }
83
84    /// Runs or re-runs the configured discovery algorithm and updates/populates the cache
85    ///
86    /// The cache is empty cleared when this is started. By default, every api group found is checked,
87    /// causing `N+2` queries to the api server (where `N` is number of api groups).
88    ///
89    /// **Note**: Consider using [`Discovery::run_aggregated`] instead, which only requires
90    /// 2 API calls regardless of the number of groups (requires Kubernetes 1.26+).
91    ///
92    /// ```no_run
93    /// use kube::{Client, api::{Api, DynamicObject}, discovery::{Discovery, verbs, Scope}, ResourceExt};
94    /// #[tokio::main]
95    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
96    ///     let client = Client::try_default().await?;
97    ///     let discovery = Discovery::new(client.clone()).run().await?;
98    ///     for group in discovery.groups() {
99    ///         for (ar, caps) in group.recommended_resources() {
100    ///             if !caps.supports_operation(verbs::LIST) {
101    ///                 continue;
102    ///             }
103    ///             let api: Api<DynamicObject> = Api::all_with(client.clone(), &ar);
104    ///             // can now api.list() to emulate kubectl get all --all
105    ///             for obj in api.list(&Default::default()).await? {
106    ///                 println!("{} {}: {}", ar.api_version, ar.kind, obj.name_any());
107    ///             }
108    ///         }
109    ///     }
110    ///     Ok(())
111    /// }
112    /// ```
113    /// See a bigger example in [examples/dynamic.api](https://github.com/kube-rs/kube/blob/main/examples/dynamic_api.rs)
114    pub async fn run(mut self) -> Result<Self> {
115        self.groups.clear();
116        let api_groups = self.client.list_api_groups().await?;
117        // query regular groups + crds under /apis
118        for g in api_groups.groups {
119            let key = g.name.clone();
120            if self.mode.is_queryable(&key) {
121                let apigroup = ApiGroup::query_apis(&self.client, g).await?;
122                self.groups.insert(key, apigroup);
123            }
124        }
125        // query core versions under /api
126        let corekey = ApiGroup::CORE_GROUP.to_string();
127        if self.mode.is_queryable(&corekey) {
128            let coreapis = self.client.list_core_api_versions().await?;
129            let apigroup = ApiGroup::query_core(&self.client, coreapis).await?;
130            self.groups.insert(corekey, apigroup);
131        }
132        Ok(self)
133    }
134
135    /// Runs discovery using the Aggregated Discovery API
136    ///
137    /// This method uses the Aggregated Discovery API (available since Kubernetes 1.26, stable in 1.30)
138    /// to fetch all API resources in just 2 requests instead of N+2 requests.
139    ///
140    /// The Aggregated Discovery API provides all resource information in the response to `/api` and `/apis`
141    /// when the appropriate Accept header is set, eliminating the need to query each group individually.
142    ///
143    /// ```no_run
144    /// use kube::{Client, api::{Api, DynamicObject}, discovery::{Discovery, verbs, Scope}, ResourceExt};
145    /// #[tokio::main]
146    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
147    ///     let client = Client::try_default().await?;
148    ///     // Uses only 2 API calls instead of N+2
149    ///     let discovery = Discovery::new(client.clone()).run_aggregated().await?;
150    ///     for group in discovery.groups() {
151    ///         for (ar, caps) in group.recommended_resources() {
152    ///             if !caps.supports_operation(verbs::LIST) {
153    ///                 continue;
154    ///             }
155    ///             let api: Api<DynamicObject> = Api::all_with(client.clone(), &ar);
156    ///             for obj in api.list(&Default::default()).await? {
157    ///                 println!("{} {}: {}", ar.api_version, ar.kind, obj.name_any());
158    ///             }
159    ///         }
160    ///     }
161    ///     Ok(())
162    /// }
163    /// ```
164    ///
165    /// # Requirements
166    /// - Kubernetes 1.26+ (beta) or 1.30+ (stable)
167    ///
168    /// # Note
169    /// If the server does not support Aggregated Discovery, this will return an error.
170    /// Consider falling back to [`Discovery::run`] in that case.
171    pub async fn run_aggregated(mut self) -> Result<Self> {
172        self.groups.clear();
173
174        // Query /apis for all non-core groups (single request)
175        let apis_discovery = self.client.list_api_groups_aggregated().await?;
176        for ag in apis_discovery.items {
177            let key = ag
178                .metadata
179                .as_ref()
180                .and_then(|m| m.name.clone())
181                .unwrap_or_default();
182            if self.mode.is_queryable(&key) {
183                let apigroup = ApiGroup::from_v2(ag)?;
184                self.groups.insert(key, apigroup);
185            }
186        }
187
188        // Query /api for core group (single request)
189        let corekey = ApiGroup::CORE_GROUP.to_string();
190        if self.mode.is_queryable(&corekey) {
191            let core_discovery = self.client.list_core_api_versions_aggregated().await?;
192            // Core group is the first (and usually only) item
193            if let Some(core_ag) = core_discovery.items.into_iter().next() {
194                let apigroup = ApiGroup::from_v2(core_ag)?;
195                self.groups.insert(corekey, apigroup);
196            }
197        }
198
199        Ok(self)
200    }
201}
202
203/// Interface to the Discovery cache
204impl Discovery {
205    /// Returns iterator over all served groups
206    pub fn groups(&self) -> impl Iterator<Item = &ApiGroup> {
207        self.groups.values()
208    }
209
210    /// Returns a sorted vector of all served groups
211    ///
212    /// This vector is in kubectl's normal alphabetical group order
213    pub fn groups_alphabetical(&self) -> Vec<&ApiGroup> {
214        let mut values: Vec<_> = self.groups().collect();
215        // collect to maintain kubectl order of groups
216        values.sort_by_key(|g| g.name());
217        values
218    }
219
220    /// Returns the [`ApiGroup`] for a given group if served
221    pub fn get(&self, group: &str) -> Option<&ApiGroup> {
222        self.groups.get(group)
223    }
224
225    /// Check if a group is served by the apiserver
226    pub fn has_group(&self, group: &str) -> bool {
227        self.groups.contains_key(group)
228    }
229
230    /// Finds an [`ApiResource`] and its [`ApiCapabilities`] after discovery by matching a GVK
231    ///
232    /// This is for quick extraction after having done a complete discovery.
233    /// If you are only interested in a single kind, consider [`oneshot::pinned_kind`](crate::discovery::pinned_kind).
234    pub fn resolve_gvk(&self, gvk: &GroupVersionKind) -> Option<(ApiResource, ApiCapabilities)> {
235        self.get(&gvk.group)?
236            .versioned_resources(&gvk.version)
237            .into_iter()
238            .find(|res| res.0.kind == gvk.kind)
239    }
240}