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 kube_core::{DynamicObject, TypeMeta};
7use std::collections::HashMap;
8mod apigroup;
9pub mod oneshot;
10pub use apigroup::ApiGroup;
11mod parse;
12
13// re-export one-shots
14pub use oneshot::{group, pinned_group, pinned_kind};
15
16/// How the Discovery client decides what api groups to scan
17enum DiscoveryMode {
18 /// Only allow explicitly listed apigroups
19 Allow(Vec<String>),
20 /// Allow all apigroups except the ones listed
21 Block(Vec<String>),
22}
23
24impl DiscoveryMode {
25 fn is_queryable(&self, group: &String) -> bool {
26 match &self {
27 Self::Allow(allowed) => allowed.contains(group),
28 Self::Block(blocked) => !blocked.contains(group),
29 }
30 }
31}
32
33/// A caching client for running API discovery against the Kubernetes API.
34///
35/// This simplifies the required querying and type matching, and stores the responses
36/// for each discovered api group and exposes helpers to access them.
37///
38/// The discovery process varies in complexity depending on:
39/// - how much you know about the kind(s) and group(s) you are interested in
40/// - how many groups you are interested in
41///
42/// Discovery can be performed on:
43/// - all api groups (default)
44/// - a subset of api groups (by setting Discovery::filter)
45///
46/// To make use of discovered apis, extract one or more [`ApiGroup`]s from it,
47/// or resolve a precise one using [`Discovery::resolve_gvk`](crate::discovery::Discovery::resolve_gvk).
48///
49/// If caching of results is __not required__, then a simpler [`oneshot`](crate::discovery::oneshot) discovery system can be used.
50///
51/// [`ApiGroup`]: crate::discovery::ApiGroup
52#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
53pub struct Discovery {
54 client: Client,
55 groups: HashMap<String, ApiGroup>,
56 mode: DiscoveryMode,
57}
58
59/// Caching discovery interface
60///
61/// Builds an internal map of its cache
62impl Discovery {
63 /// Construct a caching api discovery client
64 #[must_use]
65 pub fn new(client: Client) -> Self {
66 let groups = HashMap::new();
67 let mode = DiscoveryMode::Block(vec![]);
68 Self { client, groups, mode }
69 }
70
71 /// Configure the discovery client to only look for the listed apigroups
72 #[must_use]
73 pub fn filter(mut self, allow: &[&str]) -> Self {
74 self.mode = DiscoveryMode::Allow(allow.iter().map(ToString::to_string).collect());
75 self
76 }
77
78 /// Configure the discovery client to look for all apigroups except the listed ones
79 #[must_use]
80 pub fn exclude(mut self, deny: &[&str]) -> Self {
81 self.mode = DiscoveryMode::Block(deny.iter().map(ToString::to_string).collect());
82 self
83 }
84
85 /// Runs or re-runs the configured discovery algorithm and updates/populates the cache
86 ///
87 /// The cache is empty cleared when this is started. By default, every api group found is checked,
88 /// causing `N+2` queries to the api server (where `N` is number of api groups).
89 ///
90 /// **Note**: Consider using [`Discovery::run_aggregated`] instead, which only requires
91 /// 2 API calls regardless of the number of groups (requires Kubernetes 1.26+).
92 ///
93 /// ```no_run
94 /// use kube::{Client, api::{Api, DynamicObject}, discovery::{Discovery, verbs, Scope}, ResourceExt};
95 /// #[tokio::main]
96 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
97 /// let client = Client::try_default().await?;
98 /// let discovery = Discovery::new(client.clone()).run().await?;
99 /// for group in discovery.groups() {
100 /// for (ar, caps) in group.recommended_resources() {
101 /// if !caps.supports_operation(verbs::LIST) {
102 /// continue;
103 /// }
104 /// let api: Api<DynamicObject> = Api::all_with(client.clone(), &ar);
105 /// // can now api.list() to emulate kubectl get all --all
106 /// for obj in api.list(&Default::default()).await? {
107 /// println!("{} {}: {}", ar.api_version, ar.kind, obj.name_any());
108 /// }
109 /// }
110 /// }
111 /// Ok(())
112 /// }
113 /// ```
114 /// See a bigger example in [examples/dynamic.api](https://github.com/kube-rs/kube/blob/main/examples/dynamic_api.rs)
115 pub async fn run(mut self) -> Result<Self> {
116 self.groups.clear();
117 let api_groups = self.client.list_api_groups().await?;
118 // query regular groups + crds under /apis
119 for g in api_groups.groups {
120 let key = g.name.clone();
121 if self.mode.is_queryable(&key) {
122 let apigroup = ApiGroup::query_apis(&self.client, g).await?;
123 self.groups.insert(key, apigroup);
124 }
125 }
126 // query core versions under /api
127 let corekey = ApiGroup::CORE_GROUP.to_string();
128 if self.mode.is_queryable(&corekey) {
129 let coreapis = self.client.list_core_api_versions().await?;
130 let apigroup = ApiGroup::query_core(&self.client, coreapis).await?;
131 self.groups.insert(corekey, apigroup);
132 }
133 Ok(self)
134 }
135
136 /// Runs discovery using the Aggregated Discovery API
137 ///
138 /// This method uses the Aggregated Discovery API (available since Kubernetes 1.26, stable in 1.30)
139 /// to fetch all API resources in just 2 requests instead of N+2 requests.
140 ///
141 /// The Aggregated Discovery API provides all resource information in the response to `/api` and `/apis`
142 /// when the appropriate Accept header is set, eliminating the need to query each group individually.
143 ///
144 /// ```no_run
145 /// use kube::{Client, api::{Api, DynamicObject}, discovery::{Discovery, verbs, Scope}, ResourceExt};
146 /// #[tokio::main]
147 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
148 /// let client = Client::try_default().await?;
149 /// // Uses only 2 API calls instead of N+2
150 /// let discovery = Discovery::new(client.clone()).run_aggregated().await?;
151 /// for group in discovery.groups() {
152 /// for (ar, caps) in group.recommended_resources() {
153 /// if !caps.supports_operation(verbs::LIST) {
154 /// continue;
155 /// }
156 /// let api: Api<DynamicObject> = Api::all_with(client.clone(), &ar);
157 /// for obj in api.list(&Default::default()).await? {
158 /// println!("{} {}: {}", ar.api_version, ar.kind, obj.name_any());
159 /// }
160 /// }
161 /// }
162 /// Ok(())
163 /// }
164 /// ```
165 ///
166 /// # Requirements
167 /// - Kubernetes 1.26+ (beta) or 1.30+ (stable)
168 ///
169 /// # Note
170 /// If the server does not support Aggregated Discovery, this will return an error.
171 /// Consider falling back to [`Discovery::run`] in that case.
172 pub async fn run_aggregated(mut self) -> Result<Self> {
173 self.groups.clear();
174
175 // Query /apis for all non-core groups (single request)
176 let apis_discovery = self.client.list_api_groups_aggregated().await?;
177 for ag in apis_discovery.items {
178 let key = ag
179 .metadata
180 .as_ref()
181 .and_then(|m| m.name.clone())
182 .unwrap_or_default();
183 if self.mode.is_queryable(&key) {
184 let apigroup = ApiGroup::from_v2(ag)?;
185 self.groups.insert(key, apigroup);
186 }
187 }
188
189 // Query /api for core group (single request)
190 let corekey = ApiGroup::CORE_GROUP.to_string();
191 if self.mode.is_queryable(&corekey) {
192 let core_discovery = self.client.list_core_api_versions_aggregated().await?;
193 // Core group is the first (and usually only) item
194 if let Some(core_ag) = core_discovery.items.into_iter().next() {
195 let apigroup = ApiGroup::from_v2(core_ag)?;
196 self.groups.insert(corekey, apigroup);
197 }
198 }
199
200 Ok(self)
201 }
202}
203
204/// Interface to the Discovery cache
205impl Discovery {
206 /// Returns iterator over all served groups
207 pub fn groups(&self) -> impl Iterator<Item = &ApiGroup> {
208 self.groups.values()
209 }
210
211 /// Returns a sorted vector of all served groups
212 ///
213 /// This vector is in kubectl's normal alphabetical group order
214 pub fn groups_alphabetical(&self) -> Vec<&ApiGroup> {
215 let mut values: Vec<_> = self.groups().collect();
216 // collect to maintain kubectl order of groups
217 values.sort_by_key(|g| g.name());
218 values
219 }
220
221 /// Returns the [`ApiGroup`] for a given group if served
222 pub fn get(&self, group: &str) -> Option<&ApiGroup> {
223 self.groups.get(group)
224 }
225
226 /// Check if a group is served by the apiserver
227 pub fn has_group(&self, group: &str) -> bool {
228 self.groups.contains_key(group)
229 }
230
231 /// Finds an [`ApiResource`] and its [`ApiCapabilities`] after discovery by matching a GVK
232 ///
233 /// This is for quick extraction after having done a complete discovery.
234 /// If you are only interested in a single kind, consider [`oneshot::pinned_kind`](crate::discovery::pinned_kind).
235 pub fn resolve_gvk(&self, gvk: &GroupVersionKind) -> Option<(ApiResource, ApiCapabilities)> {
236 self.get(&gvk.group)?
237 .versioned_resources(&gvk.version)
238 .into_iter()
239 .find(|res| res.0.kind == gvk.kind)
240 }
241
242 /// Finds an [`ApiResource`] and its [`ApiCapabilities`] from a [`TypeMeta`] after discovery
243 ///
244 /// A convenience wrapper around [`Discovery::resolve_gvk`] for when you have a [`TypeMeta`]
245 /// (e.g. taken from a [`DynamicObject`]) rather than a [`GroupVersionKind`]. Returns `None` if
246 /// the `apiVersion` cannot be parsed or the kind was not discovered.
247 pub fn resolve_typemeta(&self, tm: &TypeMeta) -> Option<(ApiResource, ApiCapabilities)> {
248 let gvk = GroupVersionKind::try_from(tm).ok()?;
249 self.resolve_gvk(&gvk)
250 }
251
252 /// Finds an [`ApiResource`] and its [`ApiCapabilities`] for a [`DynamicObject`] after discovery
253 ///
254 /// This makes it easy to build an [`Api`](crate::Api) for an object obtained dynamically
255 /// (e.g. from an admission request or another watch) without manually assembling a
256 /// [`GroupVersionKind`]:
257 ///
258 /// ```no_run
259 /// use kube::{Client, api::{Api, DynamicObject}, discovery::Discovery};
260 /// #[tokio::main]
261 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
262 /// let client = Client::try_default().await?;
263 /// let object: DynamicObject = todo!("an object obtained at runtime");
264 /// let discovery = Discovery::new(client.clone()).run().await?;
265 /// if let Some((ar, _caps)) = discovery.resolve_object(&object) {
266 /// let api: Api<DynamicObject> = match object.metadata.namespace.as_deref() {
267 /// Some(ns) => Api::namespaced_with(client.clone(), ns, &ar),
268 /// None => Api::all_with(client.clone(), &ar),
269 /// };
270 /// // now `api` can be used to interact with the object
271 /// let _ = api;
272 /// }
273 /// Ok(())
274 /// }
275 /// ```
276 ///
277 /// Returns `None` if the object has no [`TypeMeta`], its `apiVersion` cannot be parsed, or the
278 /// kind was not discovered.
279 pub fn resolve_object(&self, obj: &DynamicObject) -> Option<(ApiResource, ApiCapabilities)> {
280 self.resolve_typemeta(obj.types.as_ref()?)
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use crate::client::Body;
288 use http::{Request, Response};
289 use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
290 use kube_core::{
291 DynamicObject, TypeMeta,
292 discovery::v2::{
293 APIGroupDiscovery, APIResourceDiscovery, APIVersionDiscovery, GroupVersionKind as V2Gvk,
294 },
295 };
296 use tower_test::mock;
297
298 // Build a Discovery cache serving a single apps/v1 Deployment, with a dummy client
299 // (resolve_typemeta / resolve_object never touch the client).
300 fn test_discovery() -> Discovery {
301 let ag = APIGroupDiscovery {
302 metadata: Some(ObjectMeta {
303 name: Some("apps".to_string()),
304 ..Default::default()
305 }),
306 versions: vec![APIVersionDiscovery {
307 version: Some("v1".to_string()),
308 resources: vec![APIResourceDiscovery {
309 resource: Some("deployments".to_string()),
310 response_kind: Some(V2Gvk {
311 group: None,
312 version: None,
313 kind: Some("Deployment".to_string()),
314 }),
315 scope: Some("Namespaced".to_string()),
316 verbs: vec!["get".to_string(), "list".to_string()],
317 ..Default::default()
318 }],
319 freshness: Some("Current".to_string()),
320 }],
321 };
322 let mut groups = HashMap::new();
323 groups.insert("apps".to_string(), ApiGroup::from_v2(ag).unwrap());
324 let (mock_service, _handle) = mock::pair::<Request<Body>, Response<Body>>();
325 let client = Client::new(mock_service, "default");
326 Discovery {
327 client,
328 groups,
329 mode: DiscoveryMode::Block(vec![]),
330 }
331 }
332
333 #[tokio::test]
334 async fn resolve_typemeta_finds_resource() {
335 let discovery = test_discovery();
336 let tm = TypeMeta {
337 api_version: "apps/v1".to_string(),
338 kind: "Deployment".to_string(),
339 };
340 let (ar, _caps) = discovery.resolve_typemeta(&tm).expect("known typemeta resolves");
341 assert_eq!(ar.group, "apps");
342 assert_eq!(ar.version, "v1");
343 assert_eq!(ar.kind, "Deployment");
344 }
345
346 #[tokio::test]
347 async fn resolve_object_finds_resource() {
348 let discovery = test_discovery();
349 let obj = DynamicObject {
350 types: Some(TypeMeta {
351 api_version: "apps/v1".to_string(),
352 kind: "Deployment".to_string(),
353 }),
354 metadata: Default::default(),
355 data: Default::default(),
356 };
357 let (ar, _caps) = discovery.resolve_object(&obj).expect("object with types resolves");
358 assert_eq!(ar.kind, "Deployment");
359 assert_eq!(ar.api_version, "apps/v1");
360 }
361
362 #[tokio::test]
363 async fn resolve_object_without_typemeta_is_none() {
364 let discovery = test_discovery();
365 let obj = DynamicObject {
366 types: None,
367 metadata: Default::default(),
368 data: Default::default(),
369 };
370 assert!(discovery.resolve_object(&obj).is_none());
371 }
372
373 #[tokio::test]
374 async fn resolve_typemeta_unknown_kind_is_none() {
375 let discovery = test_discovery();
376 let tm = TypeMeta {
377 api_version: "apps/v1".to_string(),
378 kind: "DoesNotExist".to_string(),
379 };
380 assert!(discovery.resolve_typemeta(&tm).is_none());
381 }
382}