use super::parse::{self, GroupVersionData};
use crate::{Client, Error, Result, error::DiscoveryError};
use k8s_openapi::apimachinery::pkg::apis::meta::v1::{APIGroup, APIVersions};
pub use kube_core::discovery::{ApiCapabilities, ApiResource};
use kube_core::{
Version,
discovery::v2::APIGroupDiscovery,
gvk::{GroupVersion, GroupVersionKind, ParseGroupVersionError},
};
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)
}
pub(crate) fn from_v2(ag: APIGroupDiscovery) -> Result<Self> {
let name = ag.metadata.and_then(|m| m.name).unwrap_or_default();
if ag.versions.is_empty() {
return Err(Error::Discovery(DiscoveryError::EmptyApiGroup(name)));
}
let preferred = ag.versions.first().and_then(|v| v.version.clone());
let data: Vec<GroupVersionData> = ag
.versions
.into_iter()
.map(|ver| GroupVersionData::from_v2(&name, ver))
.collect();
let mut group = ApiGroup {
name,
data,
preferred,
};
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 k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
use kube_core::discovery::{
Scope,
v2::{APIGroupDiscovery, APIResourceDiscovery, APIVersionDiscovery, GroupVersionKind},
};
fn make_v2_resource(resource: &str, kind: &str, scope: &str, verbs: Vec<&str>) -> APIResourceDiscovery {
APIResourceDiscovery {
resource: Some(resource.to_string()),
response_kind: Some(GroupVersionKind {
group: None,
version: None,
kind: Some(kind.to_string()),
}),
scope: Some(scope.to_string()),
verbs: verbs.into_iter().map(String::from).collect(),
..Default::default()
}
}
#[test]
fn test_api_group_from_v2_apps() {
let ag = APIGroupDiscovery {
metadata: Some(ObjectMeta {
name: Some("apps".to_string()),
..Default::default()
}),
versions: vec![APIVersionDiscovery {
version: Some("v1".to_string()),
resources: vec![
make_v2_resource("deployments", "Deployment", "Namespaced", vec![
"get", "list", "create",
]),
make_v2_resource("replicasets", "ReplicaSet", "Namespaced", vec!["get", "list"]),
],
freshness: Some("Current".to_string()),
}],
};
let group = ApiGroup::from_v2(ag).unwrap();
assert_eq!(group.name(), "apps");
assert_eq!(group.preferred_version(), Some("v1"));
assert_eq!(group.versions().collect::<Vec<_>>(), vec!["v1"]);
let resources = group.recommended_resources();
assert_eq!(resources.len(), 2);
let (deploy_ar, deploy_caps) = group.recommended_kind("Deployment").unwrap();
assert_eq!(deploy_ar.group, "apps");
assert_eq!(deploy_ar.version, "v1");
assert_eq!(deploy_ar.api_version, "apps/v1");
assert_eq!(deploy_ar.kind, "Deployment");
assert_eq!(deploy_caps.scope, Scope::Namespaced);
}
#[test]
fn test_api_group_from_v2_core() {
let ag = APIGroupDiscovery {
metadata: Some(ObjectMeta {
name: Some("".to_string()), ..Default::default()
}),
versions: vec![APIVersionDiscovery {
version: Some("v1".to_string()),
resources: vec![
make_v2_resource("pods", "Pod", "Namespaced", vec!["get", "list", "watch"]),
make_v2_resource("nodes", "Node", "Cluster", vec!["get", "list"]),
],
freshness: Some("Current".to_string()),
}],
};
let group = ApiGroup::from_v2(ag).unwrap();
assert_eq!(group.name(), "");
assert_eq!(group.preferred_version(), Some("v1"));
let (pod_ar, pod_caps) = group.recommended_kind("Pod").unwrap();
assert_eq!(pod_ar.group, "");
assert_eq!(pod_ar.api_version, "v1"); assert_eq!(pod_caps.scope, Scope::Namespaced);
let (node_ar, node_caps) = group.recommended_kind("Node").unwrap();
assert_eq!(node_ar.kind, "Node");
assert_eq!(node_caps.scope, Scope::Cluster);
}
#[test]
fn test_api_group_from_v2_multiple_versions() {
let ag = APIGroupDiscovery {
metadata: Some(ObjectMeta {
name: Some("autoscaling".to_string()),
..Default::default()
}),
versions: vec![
APIVersionDiscovery {
version: Some("v2".to_string()),
resources: vec![make_v2_resource(
"horizontalpodautoscalers",
"HorizontalPodAutoscaler",
"Namespaced",
vec!["get", "list"],
)],
freshness: Some("Current".to_string()),
},
APIVersionDiscovery {
version: Some("v1".to_string()),
resources: vec![make_v2_resource(
"horizontalpodautoscalers",
"HorizontalPodAutoscaler",
"Namespaced",
vec!["get"],
)],
freshness: Some("Current".to_string()),
},
],
};
let group = ApiGroup::from_v2(ag).unwrap();
assert_eq!(group.name(), "autoscaling");
assert_eq!(group.preferred_version(), Some("v2"));
assert_eq!(group.versions().collect::<Vec<_>>(), vec!["v2", "v1"]);
let (ar, _) = group.recommended_kind("HorizontalPodAutoscaler").unwrap();
assert_eq!(ar.version, "v2");
let v1_resources = group.versioned_resources("v1");
assert_eq!(v1_resources.len(), 1);
assert_eq!(v1_resources[0].0.version, "v1");
}
#[test]
fn test_api_group_from_v2_empty_versions_error() {
let ag = APIGroupDiscovery {
metadata: Some(ObjectMeta {
name: Some("empty".to_string()),
..Default::default()
}),
versions: vec![], };
let result = ApiGroup::from_v2(ag);
assert!(result.is_err());
}
#[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"
);
}
}