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}