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}