use super::parse::{self, GroupVersionData};
use crate::{error::DiscoveryError, Client, Error, Result};
use k8s_openapi::apimachinery::pkg::apis::meta::v1::{APIGroup, APIVersions};
pub use kube_core::discovery::{ApiCapabilities, ApiResource};
use kube_core::{
gvk::{GroupVersion, GroupVersionKind, ParseGroupVersionError},
Version,
};
use std::{cmp::Reverse, collections::HashMap, iter::Iterator};
pub struct ApiGroup {
name: String,
data: Vec<GroupVersionData>,
preferred: Option<String>,
}
impl ApiGroup {
pub(crate) async fn query_apis(client: &Client, g: APIGroup) -> Result<Self> {
tracing::debug!(name = g.name.as_str(), "Listing group versions");
let key = g.name;
if g.versions.is_empty() {
return Err(Error::Discovery(DiscoveryError::EmptyApiGroup(key)));
}
let mut data = vec![];
for vers in &g.versions {
let resources = client.list_api_group_resources(&vers.group_version).await?;
data.push(GroupVersionData::new(vers.version.clone(), resources)?);
}
let mut group = ApiGroup {
name: key,
data,
preferred: g.preferred_version.map(|v| v.version),
};
group.sort_versions();
Ok(group)
}
pub(crate) async fn query_core(client: &Client, coreapis: APIVersions) -> Result<Self> {
let mut data = vec![];
let key = ApiGroup::CORE_GROUP.to_string();
if coreapis.versions.is_empty() {
return Err(Error::Discovery(DiscoveryError::EmptyApiGroup(key)));
}
for v in coreapis.versions {
let resources = client.list_core_api_resources(&v).await?;
data.push(GroupVersionData::new(v, resources)?);
}
let mut group = ApiGroup {
name: ApiGroup::CORE_GROUP.to_string(),
data,
preferred: Some("v1".to_string()),
};
group.sort_versions();
Ok(group)
}
fn sort_versions(&mut self) {
self.data
.sort_by_cached_key(|gvd| Reverse(Version::parse(gvd.version.as_str()).priority()))
}
pub(crate) async fn query_gvk(
client: &Client,
gvk: &GroupVersionKind,
) -> Result<(ApiResource, ApiCapabilities)> {
let apiver = gvk.api_version();
let list = if gvk.group.is_empty() {
client.list_core_api_resources(&apiver).await?
} else {
client.list_api_group_resources(&apiver).await?
};
for res in &list.resources {
if res.kind == gvk.kind && !res.name.contains('/') {
let ar = parse::parse_apiresource(res, &list.group_version).map_err(
|ParseGroupVersionError(s)| Error::Discovery(DiscoveryError::InvalidGroupVersion(s)),
)?;
let caps = parse::parse_apicapabilities(&list, &res.name)?;
return Ok((ar, caps));
}
}
Err(Error::Discovery(DiscoveryError::MissingKind(format!("{gvk:?}"))))
}
pub(crate) async fn query_gv(client: &Client, gv: &GroupVersion) -> Result<Self> {
let apiver = gv.api_version();
let list = if gv.group.is_empty() {
client.list_core_api_resources(&apiver).await?
} else {
client.list_api_group_resources(&apiver).await?
};
let data = GroupVersionData::new(gv.version.clone(), list)?;
let group = ApiGroup {
name: gv.group.clone(),
data: vec![data],
preferred: Some(gv.version.clone()), };
Ok(group)
}
}
impl ApiGroup {
pub const CORE_GROUP: &'static str = "";
pub fn name(&self) -> &str {
&self.name
}
pub fn versions(&self) -> impl Iterator<Item = &str> {
self.data.as_slice().iter().map(|gvd| gvd.version.as_str())
}
pub fn preferred_version(&self) -> Option<&str> {
self.preferred.as_deref()
}
pub fn preferred_version_or_latest(&self) -> &str {
self.preferred
.as_deref()
.unwrap_or_else(|| self.versions().next().unwrap())
}
pub fn versioned_resources(&self, ver: &str) -> Vec<(ApiResource, ApiCapabilities)> {
self.data
.iter()
.find(|gvd| gvd.version == ver)
.map(|gvd| gvd.resources.clone())
.unwrap_or_default()
}
pub fn recommended_resources(&self) -> Vec<(ApiResource, ApiCapabilities)> {
let ver = self.preferred_version_or_latest();
self.versioned_resources(ver)
}
pub fn resources_by_stability(&self) -> Vec<(ApiResource, ApiCapabilities)> {
let mut lookup = HashMap::new();
self.data.iter().for_each(|gvd| {
gvd.resources.iter().for_each(|resource| {
lookup
.entry(resource.0.kind.clone())
.or_insert_with(Vec::new)
.push(resource);
})
});
lookup
.into_values()
.map(|mut v| {
v.sort_by_cached_key(|(ar, _)| Reverse(Version::parse(ar.version.as_str()).priority()));
v[0].to_owned()
})
.collect()
}
pub fn recommended_kind(&self, kind: &str) -> Option<(ApiResource, ApiCapabilities)> {
let ver = self.preferred_version_or_latest();
for (ar, caps) in self.versioned_resources(ver) {
if ar.kind == kind {
return Some((ar, caps));
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use kube_core::discovery::Scope;
#[test]
fn test_resources_by_stability() {
let ac = ApiCapabilities {
scope: Scope::Namespaced,
subresources: vec![],
operations: vec![],
};
let testlowversioncr_v1alpha1 = ApiResource {
group: String::from("kube.rs"),
version: String::from("v1alpha1"),
kind: String::from("TestLowVersionCr"),
api_version: String::from("kube.rs/v1alpha1"),
plural: String::from("testlowversioncrs"),
};
let testcr_v1 = ApiResource {
group: String::from("kube.rs"),
version: String::from("v1"),
kind: String::from("TestCr"),
api_version: String::from("kube.rs/v1"),
plural: String::from("testcrs"),
};
let testcr_v2alpha1 = ApiResource {
group: String::from("kube.rs"),
version: String::from("v2alpha1"),
kind: String::from("TestCr"),
api_version: String::from("kube.rs/v2alpha1"),
plural: String::from("testcrs"),
};
let group = ApiGroup {
name: "kube.rs".to_string(),
data: vec![
GroupVersionData {
version: "v1alpha1".to_string(),
resources: vec![(testlowversioncr_v1alpha1, ac.clone())],
},
GroupVersionData {
version: "v1".to_string(),
resources: vec![(testcr_v1, ac.clone())],
},
GroupVersionData {
version: "v2alpha1".to_string(),
resources: vec![(testcr_v2alpha1, ac)],
},
],
preferred: Some(String::from("v1")),
};
let resources = group.resources_by_stability();
assert!(
resources
.iter()
.any(|(ar, _)| ar.kind == "TestCr" && ar.version == "v1"),
"wrong stable version"
);
assert!(
resources
.iter()
.any(|(ar, _)| ar.kind == "TestLowVersionCr" && ar.version == "v1alpha1"),
"lost low version resource"
);
}
}