diff --git a/examples/crd_derive_multi.rs b/examples/crd_derive_multi.rs index 258eabb1a..f96342cc0 100644 --- a/examples/crd_derive_multi.rs +++ b/examples/crd_derive_multi.rs @@ -81,7 +81,6 @@ async fn main() -> anyhow::Result<()> { let newvarv2_2 = v2api.patch("new", &ssapply, &Patch::Apply(&v2m)).await?; info!("new on v2 correct on reapply to v2: {:?}", newvarv2_2.spec); - // note we can apply old versions without them being truncated to the v2 schema // in our case this means we cannot fetch them with our v1 schema (breaking change to not have oldprop) let v1m2 = v1::ManyDerive::new("old", v1::ManyDeriveSpec { @@ -101,7 +100,6 @@ async fn main() -> anyhow::Result<()> { Ok(()) } - async fn apply_crd(client: Client, crd: CustomResourceDefinition) -> anyhow::Result<()> { let crds: Api = Api::all(client.clone()); info!("Creating crd: {}", serde_yaml::to_string(&crd)?); diff --git a/examples/dynamic_api.rs b/examples/dynamic_api.rs index 879417528..e7111cca9 100644 --- a/examples/dynamic_api.rs +++ b/examples/dynamic_api.rs @@ -2,7 +2,7 @@ use kube::{ api::{Api, DynamicObject, ResourceExt}, - discovery::{verbs, Discovery, Scope}, + discovery::{verbs, Discovery}, Client, }; use tracing::*; @@ -14,14 +14,15 @@ async fn main() -> anyhow::Result<()> { let discovery = Discovery::new(client.clone()).run().await?; for group in discovery.groups() { - for (ar, caps) in group.recommended_resources() { - if !caps.supports_operation(verbs::LIST) { + for ar in group.recommended_resources() { + if !ar.supports_operation(verbs::LIST) { continue; } - let api: Api = if caps.scope == Scope::Cluster { - Api::all_with(client.clone(), &ar) - } else { + + let api: Api = if ar.namespaced() { Api::default_namespaced_with(client.clone(), &ar) + } else { + Api::all_with(client.clone(), &ar) }; info!("{}/{} : {}", group.name(), ar.version, ar.kind); diff --git a/examples/dynamic_watcher.rs b/examples/dynamic_watcher.rs index 2b669283a..2f3f60ddb 100644 --- a/examples/dynamic_watcher.rs +++ b/examples/dynamic_watcher.rs @@ -1,7 +1,7 @@ use futures::{StreamExt, TryStreamExt}; use kube::{ api::{Api, DynamicObject, GroupVersionKind, ListParams, ResourceExt}, - discovery::{self, Scope}, + discovery, runtime::{watcher, WatchStreamExt}, Client, }; @@ -22,7 +22,7 @@ async fn main() -> anyhow::Result<()> { // Turn them into a GVK let gvk = GroupVersionKind::gvk(&group, &version, &kind); // Use API discovery to identify more information about the type (like its plural) - let (ar, caps) = discovery::pinned_kind(&client, &gvk).await?; + let ar = discovery::pinned_kind(&client, &gvk).await?; // Use the full resource info to create an Api with the ApiResource as its DynamicType let api = Api::::all_with(client, &ar); @@ -30,10 +30,10 @@ async fn main() -> anyhow::Result<()> { // Fully compatible with kube-runtime let mut items = watcher(api, ListParams::default()).applied_objects().boxed(); while let Some(p) = items.try_next().await? { - if caps.scope == Scope::Cluster { - info!("saw {}", p.name_any()); + if let Some(ns) = p.namespace() { + info!("saw {} in {}", p.name_any(), ns); } else { - info!("saw {} in {}", p.name_any(), p.namespace().unwrap()); + info!("saw {}", p.name_any()); } } Ok(()) diff --git a/examples/kubectl.rs b/examples/kubectl.rs index 8157fa5e1..66fc5e824 100644 --- a/examples/kubectl.rs +++ b/examples/kubectl.rs @@ -10,7 +10,7 @@ use k8s_openapi::{ use kube::{ api::{Api, DynamicObject, ListParams, Patch, PatchParams, ResourceExt}, core::GroupVersionKind, - discovery::{ApiCapabilities, ApiResource, Discovery, Scope}, + discovery::{ApiResource, Discovery}, runtime::{ wait::{await_condition, conditions::is_deleted}, watcher, WatchStreamExt, @@ -66,7 +66,7 @@ enum Verb { Apply, } -fn resolve_api_resource(discovery: &Discovery, name: &str) -> Option<(ApiResource, ApiCapabilities)> { +fn resolve_api_resource(discovery: &Discovery, name: &str) -> Option { // iterate through groups to find matching kind/plural names at recommended versions // and then take the minimal match by group.name (equivalent to sorting groups by group.name). // this is equivalent to kubectl's api group preference @@ -78,7 +78,7 @@ fn resolve_api_resource(discovery: &Discovery, name: &str) -> Option<(ApiResourc .into_iter() .map(move |res| (group, res)) }) - .filter(|(_, (res, _))| { + .filter(|(_, res)| { // match on both resource name and kind name // ideally we should allow shortname matches as well name.eq_ignore_ascii_case(&res.kind) || name.eq_ignore_ascii_case(&res.plural) @@ -169,8 +169,8 @@ impl App { bail!("cannot apply object without valid TypeMeta {:?}", obj); }; let name = obj.name_any(); - if let Some((ar, caps)) = discovery.resolve_gvk(&gvk) { - let api = dynamic_api(ar, caps, client.clone(), &self.namespace, false); + if let Some(ar) = discovery.resolve_gvk(&gvk) { + let api = dynamic_api(ar, client.clone(), &self.namespace, false); trace!("Applying {}: \n{}", gvk.kind, serde_yaml::to_string(&obj)?); let data: serde_json::Value = serde_json::to_value(&obj)?; let _r = api.patch(&name, &ssapply, &Patch::Apply(data)).await?; @@ -195,13 +195,13 @@ async fn main() -> Result<()> { // Defer to methods for verbs if let Some(resource) = &app.resource { // Common discovery, parameters, and api configuration for a single resource - let (ar, caps) = resolve_api_resource(&discovery, resource) + let ar = resolve_api_resource(&discovery, resource) .with_context(|| format!("resource {:?} not found in cluster", resource))?; let mut lp = ListParams::default(); if let Some(label) = &app.selector { lp = lp.labels(label); } - let api = dynamic_api(ar, caps, client, &app.namespace, app.all); + let api = dynamic_api(ar, client, &app.namespace, app.all); tracing::info!(?app.verb, ?resource, name = ?app.name.clone().unwrap_or_default(), "requested objects"); match app.verb { @@ -217,14 +217,8 @@ async fn main() -> Result<()> { Ok(()) } -fn dynamic_api( - ar: ApiResource, - caps: ApiCapabilities, - client: Client, - ns: &Option, - all: bool, -) -> Api { - if caps.scope == Scope::Cluster || all { +fn dynamic_api(ar: ApiResource, client: Client, ns: &Option, all: bool) -> Api { + if !ar.namespaced() || all { Api::all_with(client, &ar) } else if let Some(namespace) = ns { Api::namespaced_with(client, namespace, &ar) diff --git a/kube-client/src/discovery/apigroup.rs b/kube-client/src/discovery/apigroup.rs index de03596b8..da0ef0050 100644 --- a/kube-client/src/discovery/apigroup.rs +++ b/kube-client/src/discovery/apigroup.rs @@ -1,7 +1,7 @@ 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::{verbs, ApiCapabilities, ApiResource, Scope}; +pub use kube_core::discovery::{verbs, ApiResource}; use kube_core::{ gvk::{GroupVersion, GroupVersionKind, ParseGroupVersionError}, Version, @@ -21,7 +21,7 @@ use std::{cmp::Reverse, collections::HashMap, iter::Iterator}; /// async fn main() -> Result<(), Box> { /// let client = Client::try_default().await?; /// let apigroup = discovery::group(&client, "apiregistration.k8s.io").await?; -/// for (apiresource, caps) in apigroup.versioned_resources("v1") { +/// for apiresource in apigroup.versioned_resources("v1") { /// println!("Found ApiResource {}", apiresource.kind); /// } /// Ok(()) @@ -30,12 +30,9 @@ use std::{cmp::Reverse, collections::HashMap, iter::Iterator}; /// /// But if you do not know this information, you can use [`ApiGroup::preferred_version_or_latest`]. /// -/// Whichever way you choose the end result is something describing a resource and its abilities: -/// - `Vec<(ApiResource, `ApiCapabilities)>` :: for all resources in a versioned ApiGroup -/// - `(ApiResource, ApiCapabilities)` :: for a single kind under a versioned ApiGroud -/// -/// These two types: [`ApiResource`], and [`ApiCapabilities`] -/// should contain the information needed to construct an [`Api`](crate::Api) and start querying the kubernetes API. +/// Whichever way you choose the end result is a vector of [`ApiResource`] entries per kind. +/// This [`ApiResource`] type contains the information needed to construct an [`Api`](crate::Api) +/// and start querying the kubernetes API. /// You will likely need to use [`DynamicObject`] as the generic type for Api to do this, /// as well as the [`ApiResource`] for the `DynamicType` for the [`Resource`] trait. /// @@ -45,7 +42,7 @@ use std::{cmp::Reverse, collections::HashMap, iter::Iterator}; /// async fn main() -> Result<(), Box> { /// let client = Client::try_default().await?; /// let apigroup = discovery::group(&client, "apiregistration.k8s.io").await?; -/// let (ar, caps) = apigroup.recommended_kind("APIService").unwrap(); +/// let ar = apigroup.recommended_kind("APIService").unwrap(); /// let api: Api = Api::all_with(client.clone(), &ar); /// for service in api.list(&Default::default()).await? { /// println!("Found APIService: {}", service.name()); @@ -54,7 +51,6 @@ use std::{cmp::Reverse, collections::HashMap, iter::Iterator}; /// } /// ``` /// [`ApiResource`]: crate::discovery::ApiResource -/// [`ApiCapabilities`]: crate::discovery::ApiCapabilities /// [`DynamicObject`]: crate::api::DynamicObject /// [`Resource`]: crate::Resource /// [`ApiGroup::preferred_version_or_latest`]: crate::discovery::ApiGroup::preferred_version_or_latest @@ -121,10 +117,7 @@ impl ApiGroup { } // shortcut method to give cheapest return for a single GVK - pub(crate) async fn query_gvk( - client: &Client, - gvk: &GroupVersionKind, - ) -> Result<(ApiResource, ApiCapabilities)> { + pub(crate) async fn query_gvk(client: &Client, gvk: &GroupVersionKind) -> Result { let apiver = gvk.api_version(); let list = if gvk.group.is_empty() { client.list_core_api_resources(&apiver).await? @@ -133,11 +126,11 @@ impl ApiGroup { }; for res in &list.resources { if res.kind == gvk.kind && !res.name.contains('/') { - let ar = parse::parse_apiresource(res, &list.group_version).map_err( + let mut 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)); + ar.capabilities.subresources = parse::find_subresources(&list, &res.name)?; + return Ok(ar); } } Err(Error::Discovery(DiscoveryError::MissingKind(format!( @@ -208,7 +201,7 @@ impl ApiGroup { /// /// If you are looking for the api recommended list of resources, or just on particular kind /// consider [`ApiGroup::recommended_resources`] or [`ApiGroup::recommended_kind`] instead. - pub fn versioned_resources(&self, ver: &str) -> Vec<(ApiResource, ApiCapabilities)> { + pub fn versioned_resources(&self, ver: &str) -> Vec { self.data .iter() .find(|gvd| gvd.version == ver) @@ -224,8 +217,8 @@ impl ApiGroup { /// async fn main() -> Result<(), Box> { /// let client = Client::try_default().await?; /// let apigroup = discovery::group(&client, "apiregistration.k8s.io").await?; - /// for (ar, caps) in apigroup.recommended_resources() { - /// if !caps.supports_operation(verbs::LIST) { + /// for ar in apigroup.recommended_resources() { + /// if !ar.supports_operation(verbs::LIST) { /// continue; /// } /// let api: Api = Api::all_with(client.clone(), &ar); @@ -238,7 +231,7 @@ impl ApiGroup { /// ``` /// /// This is equivalent to taking the [`ApiGroup::versioned_resources`] at the [`ApiGroup::preferred_version_or_latest`]. - pub fn recommended_resources(&self) -> Vec<(ApiResource, ApiCapabilities)> { + pub fn recommended_resources(&self) -> Vec { let ver = self.preferred_version_or_latest(); self.versioned_resources(ver) } @@ -251,25 +244,25 @@ impl ApiGroup { /// async fn main() -> Result<(), Box> { /// let client = Client::try_default().await?; /// let apigroup = discovery::group(&client, "apiregistration.k8s.io").await?; - /// for (ar, caps) in apigroup.resources_by_stability() { - /// if !caps.supports_operation(verbs::LIST) { + /// for ar in apigroup.resources_by_stability() { + /// if !ar.supports_operation(verbs::LIST) { /// continue; /// } /// let api: Api = Api::all_with(client.clone(), &ar); /// for inst in api.list(&Default::default()).await? { - /// println!("Found {}: {}", ar.kind, inst.name()); + /// println!("Found {}: {}", ar.kind, inst.name_any()); /// } /// } /// Ok(()) /// } /// ``` /// See an example in [examples/kubectl.rs](https://github.com/kube-rs/kube/blob/main/examples/kubectl.rs) - pub fn resources_by_stability(&self) -> Vec<(ApiResource, ApiCapabilities)> { + pub fn resources_by_stability(&self) -> Vec { let mut lookup = HashMap::new(); self.data.iter().for_each(|gvd| { gvd.resources.iter().for_each(|resource| { lookup - .entry(resource.0.kind.clone()) + .entry(resource.kind.clone()) .or_insert_with(Vec::new) .push(resource); }) @@ -277,7 +270,7 @@ impl ApiGroup { lookup .into_values() .map(|mut v| { - v.sort_by_cached_key(|(ar, _)| Reverse(Version::parse(ar.version.as_str()).priority())); + v.sort_by_cached_key(|ar| Reverse(Version::parse(ar.version.as_str()).priority())); v[0].to_owned() }) .collect() @@ -291,7 +284,7 @@ impl ApiGroup { /// async fn main() -> Result<(), Box> { /// let client = Client::try_default().await?; /// let apigroup = discovery::group(&client, "apiregistration.k8s.io").await?; - /// let (ar, caps) = apigroup.recommended_kind("APIService").unwrap(); + /// let ar = apigroup.recommended_kind("APIService").unwrap(); /// let api: Api = Api::all_with(client.clone(), &ar); /// for service in api.list(&Default::default()).await? { /// println!("Found APIService: {}", service.name()); @@ -301,11 +294,11 @@ impl ApiGroup { /// ``` /// /// This is equivalent to filtering the [`ApiGroup::versioned_resources`] at [`ApiGroup::preferred_version_or_latest`] against a chosen `kind`. - pub fn recommended_kind(&self, kind: &str) -> Option<(ApiResource, ApiCapabilities)> { + pub fn recommended_kind(&self, kind: &str) -> Option { let ver = self.preferred_version_or_latest(); - for (ar, caps) in self.versioned_resources(ver) { + for ar in self.versioned_resources(ver) { if ar.kind == kind { - return Some((ar, caps)); + return Some(ar); } } None @@ -314,54 +307,34 @@ impl ApiGroup { #[cfg(test)] mod tests { - use super::*; + use super::{GroupVersionKind as GVK, *}; + #[test] fn test_resources_by_stability() { - let ac = ApiCapabilities { - scope: Scope::Namespaced, - subresources: vec![], - operations: vec![], - }; + let cr_low = GVK::gvk("kube.rs", "v1alpha1", "LowCr"); + let testcr_low = ApiResource::new(&cr_low, "lowcrs"); - 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 cr_v1 = GVK::gvk("kube.rs", "v1", "TestCr"); + let testcr_v1 = ApiResource::new(&cr_v1, "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 cr_v2a1 = GVK::gvk("kube.rs", "v2alpha1", "TestCr"); + let testcr_v2alpha1 = ApiResource::new(&cr_v2a1, "testcrs"); let group = ApiGroup { - name: "kube.rs".to_string(), + name: "kube.rs".into(), data: vec![ GroupVersionData { - version: "v1alpha1".to_string(), - resources: vec![(testlowversioncr_v1alpha1, ac.clone())], + version: "v1alpha1".into(), + resources: vec![testcr_low], }, GroupVersionData { - version: "v1".to_string(), - resources: vec![(testcr_v1, ac.clone())], + version: "v1".into(), + resources: vec![testcr_v1], }, GroupVersionData { - version: "v2alpha1".to_string(), - resources: vec![(testcr_v2alpha1, ac)], + version: "v2alpha1".into(), + resources: vec![testcr_v2alpha1], }, ], preferred: Some(String::from("v1")), @@ -371,14 +344,14 @@ mod tests { assert!( resources .iter() - .any(|(ar, _)| ar.kind == "TestCr" && ar.version == "v1"), - "wrong stable version" + .any(|ar| ar.kind == "TestCr" && ar.version == "v1"), + "picked right stable version" ); assert!( resources .iter() - .any(|(ar, _)| ar.kind == "TestLowVersionCr" && ar.version == "v1alpha1"), - "lost low version resource" + .any(|ar| ar.kind == "LowCr" && ar.version == "v1alpha1"), + "got alpha resource below preferred" ); } } diff --git a/kube-client/src/discovery/mod.rs b/kube-client/src/discovery/mod.rs index 5d8a96bc0..fdb0992fc 100644 --- a/kube-client/src/discovery/mod.rs +++ b/kube-client/src/discovery/mod.rs @@ -1,7 +1,7 @@ //! High-level utilities for runtime API discovery. use crate::{Client, Result}; -pub use kube_core::discovery::{verbs, ApiCapabilities, ApiResource, Scope}; +pub use kube_core::discovery::{verbs, ApiResource}; use kube_core::gvk::GroupVersionKind; use std::collections::HashMap; mod apigroup; @@ -87,20 +87,20 @@ impl Discovery { /// causing `N+2` queries to the api server (where `N` is number of api groups). /// /// ```no_run - /// use kube::{Client, api::{Api, DynamicObject}, discovery::{Discovery, verbs, Scope}, ResourceExt}; + /// use kube::{Client, api::{Api, DynamicObject}, discovery::{Discovery, verbs}, ResourceExt}; /// #[tokio::main] /// async fn main() -> Result<(), Box> { /// let client = Client::try_default().await?; /// let discovery = Discovery::new(client.clone()).run().await?; /// for group in discovery.groups() { - /// for (ar, caps) in group.recommended_resources() { - /// if !caps.supports_operation(verbs::LIST) { + /// for ar in group.recommended_resources() { + /// if !ar.supports_operation(verbs::LIST) { /// continue; /// } /// let api: Api = Api::all_with(client.clone(), &ar); /// // can now api.list() to emulate kubectl get all --all /// for obj in api.list(&Default::default()).await? { - /// println!("{} {}: {}", ar.api_version, ar.kind, obj.name()); + /// println!("{} {}: {}", ar.api_version, ar.kind, obj.name_any()); /// } /// } /// } @@ -157,14 +157,14 @@ impl Discovery { self.groups.contains_key(group) } - /// Finds an [`ApiResource`] and its [`ApiCapabilities`] after discovery by matching a GVK + /// Finds an [`ApiResource`] after discovery by matching a GVK /// /// This is for quick extraction after having done a complete discovery. /// If you are only interested in a single kind, consider [`oneshot::pinned_kind`](crate::discovery::pinned_kind). - pub fn resolve_gvk(&self, gvk: &GroupVersionKind) -> Option<(ApiResource, ApiCapabilities)> { + pub fn resolve_gvk(&self, gvk: &GroupVersionKind) -> Option { self.get(&gvk.group)? .versioned_resources(&gvk.version) .into_iter() - .find(|res| res.0.kind == gvk.kind) + .find(|res| res.kind == gvk.kind) } } diff --git a/kube-client/src/discovery/oneshot.rs b/kube-client/src/discovery/oneshot.rs index 5cd8998fc..519f7e313 100644 --- a/kube-client/src/discovery/oneshot.rs +++ b/kube-client/src/discovery/oneshot.rs @@ -14,7 +14,7 @@ use super::ApiGroup; use crate::{error::DiscoveryError, Client, Error, Result}; use kube_core::{ - discovery::{ApiCapabilities, ApiResource}, + discovery::ApiResource, gvk::{GroupVersion, GroupVersionKind}, }; @@ -29,7 +29,7 @@ use kube_core::{ /// async fn main() -> Result<(), Box> { /// let client = Client::try_default().await?; /// let apigroup = discovery::group(&client, "apiregistration.k8s.io").await?; -/// let (ar, caps) = apigroup.recommended_kind("APIService").unwrap(); +/// let ar = apigroup.recommended_kind("APIService").unwrap(); /// let api: Api = Api::all_with(client.clone(), &ar); /// for service in api.list(&Default::default()).await? { /// println!("Found APIService: {}", service.name()); @@ -66,7 +66,7 @@ pub async fn group(client: &Client, apigroup: &str) -> Result { /// let client = Client::try_default().await?; /// let gv = "apiregistration.k8s.io/v1".parse()?; /// let apigroup = discovery::pinned_group(&client, &gv).await?; -/// let (ar, caps) = apigroup.recommended_kind("APIService").unwrap(); +/// let ar = apigroup.recommended_kind("APIService").unwrap(); /// let api: Api = Api::all_with(client.clone(), &ar); /// for service in api.list(&Default::default()).await? { /// println!("Found APIService: {}", service.name()); @@ -93,7 +93,7 @@ pub async fn pinned_group(client: &Client, gv: &GroupVersion) -> Result Result<(), Box> { /// let client = Client::try_default().await?; /// let gvk = GroupVersionKind::gvk("apiregistration.k8s.io", "v1", "APIService"); -/// let (ar, caps) = discovery::pinned_kind(&client, &gvk).await?; +/// let ar = discovery::pinned_kind(&client, &gvk).await?; /// let api: Api = Api::all_with(client.clone(), &ar); /// for service in api.list(&Default::default()).await? { /// println!("Found APIService: {}", service.name()); @@ -101,6 +101,6 @@ pub async fn pinned_group(client: &Client, gv: &GroupVersion) -> Result Result<(ApiResource, ApiCapabilities)> { +pub async fn pinned_kind(client: &Client, gvk: &GroupVersionKind) -> Result { ApiGroup::query_gvk(client, gvk).await } diff --git a/kube-client/src/discovery/parse.rs b/kube-client/src/discovery/parse.rs index 683c51311..934f2a0af 100644 --- a/kube-client/src/discovery/parse.rs +++ b/kube-client/src/discovery/parse.rs @@ -2,18 +2,22 @@ use crate::{error::DiscoveryError, Error, Result}; use k8s_openapi::apimachinery::pkg::apis::meta::v1::{APIResource, APIResourceList}; use kube_core::{ - discovery::{ApiCapabilities, ApiResource, Scope}, + discovery::{ApiCapabilities, ApiResource}, gvk::{GroupVersion, ParseGroupVersionError}, }; /// Creates an `ApiResource` from a `meta::v1::APIResource` instance + its groupversion. -/// -/// Returns a `DiscoveryError` if the passed group_version cannot be parsed pub(crate) fn parse_apiresource( ar: &APIResource, group_version: &str, ) -> Result { let gv: GroupVersion = group_version.parse()?; + let caps = ApiCapabilities { + namespaced: ar.namespaced, + verbs: ar.verbs.clone(), + shortnames: ar.short_names.clone().unwrap_or_default(), + subresources: vec![], // filled in in outer fn + }; // NB: not safe to use this with subresources (they don't have api_versions) Ok(ApiResource { group: ar.group.clone().unwrap_or_else(|| gv.group.clone()), @@ -21,24 +25,12 @@ pub(crate) fn parse_apiresource( api_version: gv.api_version(), kind: ar.kind.to_string(), plural: ar.name.clone(), + capabilities: caps, }) } -/// Creates `ApiCapabilities` from a `meta::v1::APIResourceList` instance + a name from the list. -/// -/// Returns a `DiscoveryError` if the list does not contain resource with passed `name`. -pub(crate) fn parse_apicapabilities(list: &APIResourceList, name: &str) -> Result { - let ar = list - .resources - .iter() - .find(|r| r.name == name) - .ok_or_else(|| Error::Discovery(DiscoveryError::MissingResource(name.into())))?; - let scope = if ar.namespaced { - Scope::Namespaced - } else { - Scope::Cluster - }; - +/// Scans nearby `meta::v1::APIResourceList` for subresources with a matching prefix +pub(crate) fn find_subresources(list: &APIResourceList, name: &str) -> Result> { let subresource_name_prefix = format!("{}/", name); let mut subresources = vec![]; for res in &list.resources { @@ -48,15 +40,10 @@ pub(crate) fn parse_apicapabilities(list: &APIResourceList, name: &str) -> Resul Error::Discovery(DiscoveryError::InvalidGroupVersion(s)) })?; api_resource.plural = subresource_name.to_string(); - let caps = parse_apicapabilities(list, &res.name)?; // NB: recursion - subresources.push((api_resource, caps)); + subresources.push(api_resource); } } - Ok(ApiCapabilities { - scope, - subresources, - operations: ar.verbs.clone(), - }) + Ok(subresources) } /// Internal resource information and capabilities for a particular ApiGroup at a particular version @@ -64,7 +51,7 @@ pub(crate) struct GroupVersionData { /// Pinned api version pub(crate) version: String, /// Pair of dynamic resource info along with what it supports. - pub(crate) resources: Vec<(ApiResource, ApiCapabilities)>, + pub(crate) resources: Vec, } impl GroupVersionData { @@ -72,16 +59,17 @@ impl GroupVersionData { pub(crate) fn new(version: String, list: APIResourceList) -> Result { let mut resources = vec![]; for res in &list.resources { - // skip subresources + // skip subresources (attach those to the root ar) if res.name.contains('/') { continue; } // NB: these two should be infallible from discovery when k8s api is well-behaved, but.. - let ar = parse_apiresource(res, &list.group_version).map_err(|ParseGroupVersionError(s)| { - Error::Discovery(DiscoveryError::InvalidGroupVersion(s)) - })?; - let caps = parse_apicapabilities(&list, &res.name)?; - resources.push((ar, caps)); + let mut ar = + parse_apiresource(res, &list.group_version).map_err(|ParseGroupVersionError(s)| { + Error::Discovery(DiscoveryError::InvalidGroupVersion(s)) + })?; + ar.capabilities.subresources = find_subresources(&list, &res.name)?; + resources.push(ar); } Ok(GroupVersionData { version, resources }) } diff --git a/kube-client/src/error.rs b/kube-client/src/error.rs index 4b2826459..769b3a076 100644 --- a/kube-client/src/error.rs +++ b/kube-client/src/error.rs @@ -105,10 +105,6 @@ pub enum DiscoveryError { #[error("Missing Api Group: {0}")] MissingApiGroup(String), - /// MissingResource - #[error("Missing Resource: {0}")] - MissingResource(String), - /// Empty ApiGroup #[error("Empty Api Group: {0}")] EmptyApiGroup(String), diff --git a/kube-client/src/lib.rs b/kube-client/src/lib.rs index ff80e36bb..22225430b 100644 --- a/kube-client/src/lib.rs +++ b/kube-client/src/lib.rs @@ -123,7 +123,6 @@ pub use crate::core::{CustomResourceExt, Resource, ResourceExt}; /// Re-exports from kube_core pub use kube_core as core; - // Tests that require a cluster and the complete feature set // Can be run with `cargo test -p kube-client --lib features=rustls-tls,ws -- --ignored` #[cfg(all(feature = "client", feature = "config"))] diff --git a/kube-core/src/crd.rs b/kube-core/src/crd.rs index c8bff69ad..376e3d632 100644 --- a/kube-core/src/crd.rs +++ b/kube-core/src/crd.rs @@ -222,7 +222,6 @@ pub mod v1 { served: true storage: false"#; - let c1: Crd = serde_yaml::from_str(crd1).unwrap(); let c2: Crd = serde_yaml::from_str(crd2).unwrap(); let ce: Crd = serde_yaml::from_str(expected).unwrap(); diff --git a/kube-core/src/discovery.rs b/kube-core/src/discovery.rs index fba1eba3b..7d9390647 100644 --- a/kube-core/src/discovery.rs +++ b/kube-core/src/discovery.rs @@ -4,8 +4,26 @@ use serde::{Deserialize, Serialize}; /// Information about a Kubernetes API resource /// -/// Enough information to use it like a `Resource` by passing it to the dynamic `Api` -/// constructors like `Api::all_with` and `Api::namespaced_with`. +/// Used as dynamic type info for `Resource` to allow dynamic querying on `Api` +/// via constructors like `Api::all_with` and `Api::namespaced_with`. +/// +/// Only the instances returned by either: +/// +/// - `discovery` module in kube/kube-client +/// - `CustomResource` derive in kube-derive +/// +/// will have ALL the extraneous data about shortnames, verbs, and resources. +/// +/// # Warning +/// +/// Construction through +/// - [`ApiResource::erase`] (type erasing where we have trait data) +/// - [`ApiResource::new`] (proving all essential data manually) +/// +/// Are **minimal** conveniences that will work with the Api, but will not have all the extraneous data. +/// +/// Shorter construction methods (such as manually filling in data), or fallibly converting from GVKs, +/// may even fail to query. Provide accurate `plural` and `namespaced` data to be safe. #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct ApiResource { /// Resource group, empty for core group. @@ -17,56 +35,118 @@ pub struct ApiResource { pub api_version: String, /// Singular PascalCase name of the resource pub kind: String, - /// Plural name of the resource + /// Resource name / plural name pub plural: String, + + /// Capabilities of the resource + /// + /// NB: This is only fully populated from kube-derive or api discovery + pub capabilities: ApiCapabilities, } impl ApiResource { /// Creates an ApiResource by type-erasing a Resource + /// + /// Note that this variant of constructing an `ApiResource` dodes not + /// get you verbs and available subresources. + /// If you need this, construct via discovery. pub fn erase(dt: &K::DynamicType) -> Self { + let caps = ApiCapabilities { + namespaced: K::is_namespaced(dt), + ..ApiCapabilities::default() + }; ApiResource { group: K::group(dt).to_string(), version: K::version(dt).to_string(), api_version: K::api_version(dt).to_string(), kind: K::kind(dt).to_string(), plural: K::plural(dt).to_string(), + capabilities: caps, } } - /// Creates an ApiResource from group, version, kind and plural name. - pub fn from_gvk_with_plural(gvk: &GroupVersionKind, plural: &str) -> Self { + /// Creates a new ApiResource from a GVK and a plural name + /// + /// If you are getting your values from `kube_derive` use the generated method for giving you an [`ApiResource`] + /// on [`CustomResourceExt`], or run api discovery on it via `kube::discovery`. + /// + /// This is a **minimal** test variant needed to use with the dynamic api + /// It does not know about capabilites such as verbs, subresources or shortnames. + pub fn new(gvk: &GroupVersionKind, plural: &str) -> Self { ApiResource { api_version: gvk.api_version(), group: gvk.group.clone(), version: gvk.version.clone(), kind: gvk.kind.clone(), plural: plural.to_string(), + capabilities: ApiCapabilities::default(), } } - /// Creates an ApiResource from group, version and kind. + /// Create a minimal ApiResource from a GVK as cluster scoped + /// + /// If you have a CRD via `kube_derive` use the generated method for giving you an [`ApiResource`] + /// on [`CustomResourceExt`], or consider running api discovery on it via `kube::discovery`. + /// + /// The resulting `ApiResource` **will not contain capabilities**. /// /// # Warning - /// This function will **guess** the resource plural name. - /// Usually, this is ok, but for CRDs with complex pluralisations it can fail. - /// If you are getting your values from `kube_derive` use the generated method for giving you an [`ApiResource`]. - /// Otherwise consider using [`ApiResource::from_gvk_with_plural`](crate::discovery::ApiResource::from_gvk_with_plural) - /// to explicitly set the plural, or run api discovery on it via `kube::discovery`. + /// This function is a convenience utility intended for quick experiments. + /// This function will **guess** the resource plural name which can fail + /// for CRDs with complex pluralisations. + /// + /// Consider using [`ApiResource::new`](crate::discovery::ApiResource::new) + /// to explicitly set the plural instead. pub fn from_gvk(gvk: &GroupVersionKind) -> Self { - ApiResource::from_gvk_with_plural(gvk, &to_plural(&gvk.kind.to_ascii_lowercase())) + ApiResource::new(gvk, &to_plural(&gvk.kind.to_ascii_lowercase())) + } + + /// Get the namespaced property + pub fn namespaced(&self) -> bool { + self.capabilities.namespaced + } + + /// Set the whether the resource is namsepace scoped + pub fn set_namespaced(mut self, namespaced: bool) -> Self { + self.capabilities.namespaced = namespaced; + self + } + + /// Set the shortnames + pub fn set_shortnames(mut self, shortnames: &[&str]) -> Self { + self.capabilities.shortnames = shortnames.iter().map(|x| x.to_string()).collect(); + self + } + + /// Set the allowed verbs + pub fn set_verbs(mut self, verbs: &[&str]) -> Self { + self.capabilities.verbs = verbs.iter().map(|x| x.to_string()).collect(); + self + } + + /// Set the default verbs + pub fn set_default_verbs(mut self) -> Self { + self.capabilities.verbs = verbs::DEFAULT_VERBS.iter().map(|x| x.to_string()).collect(); + self } } -/// Resource scope -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub enum Scope { - /// Objects are global - Cluster, - /// Each object lives in namespace. - Namespaced, +/// The capabilities part of an [`ApiResource`] +/// +/// This struct is populated when populated through discovery or kube-derive. +#[derive(Debug, Default, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct ApiCapabilities { + /// Whether the resource is namespaced + pub namespaced: bool, + /// Supported verbs that are queryable + pub verbs: Vec, + /// Supported shortnames + pub shortnames: Vec, + /// Supported subresources + pub subresources: Vec, } -/// Rbac verbs for ApiCapabilities +/// Rbac verbs pub mod verbs { /// Create a resource pub const CREATE: &str = "create"; @@ -84,27 +164,19 @@ pub mod verbs { pub const UPDATE: &str = "update"; /// Patch an object pub const PATCH: &str = "patch"; -} -/// Contains the capabilities of an API resource -#[derive(Debug, Clone)] -pub struct ApiCapabilities { - /// Scope of the resource - pub scope: Scope, - /// Available subresources. - /// - /// Please note that returned ApiResources are not standalone resources. - /// Their name will be of form `subresource_name`, not `resource_name/subresource_name`. - /// To work with subresources, use `Request` methods for now. - pub subresources: Vec<(ApiResource, ApiCapabilities)>, - /// Supported operations on this resource - pub operations: Vec, + /// All the default verbs + pub const DEFAULT_VERBS: &[&str; 8] = + &[CREATE, GET, LIST, WATCH, DELETE, DELETE_COLLECTION, UPDATE, PATCH]; } -impl ApiCapabilities { +impl ApiResource { /// Checks that given verb is supported on this resource. + /// + /// Note that this fn can only answer if the ApiResource + /// was constructed via kube-derive/api discovery. pub fn supports_operation(&self, operation: &str) -> bool { - self.operations.iter().any(|op| op == operation) + self.capabilities.verbs.iter().any(|op| op == operation) } } diff --git a/kube-core/src/dynamic.rs b/kube-core/src/dynamic.rs index 885b32e40..6d31ad7f0 100644 --- a/kube-core/src/dynamic.rs +++ b/kube-core/src/dynamic.rs @@ -3,10 +3,7 @@ //! For concrete usage see [examples prefixed with dynamic_](https://github.com/kube-rs/kube/tree/main/examples). pub use crate::discovery::ApiResource; -use crate::{ - metadata::TypeMeta, - resource::{DynamicResourceScope, Resource}, -}; +use crate::{metadata::TypeMeta, resource::Resource, scope::DynamicResourceScope}; use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; use std::borrow::Cow; @@ -90,6 +87,10 @@ impl Resource for DynamicObject { fn meta_mut(&mut self) -> &mut ObjectMeta { &mut self.metadata } + + fn is_namespaced(dt: &ApiResource) -> bool { + dt.capabilities.namespaced + } } #[cfg(test)] diff --git a/kube-core/src/lib.rs b/kube-core/src/lib.rs index 71b81b917..070fdafe3 100644 --- a/kube-core/src/lib.rs +++ b/kube-core/src/lib.rs @@ -17,6 +17,7 @@ pub mod admission; pub mod conversion; pub mod discovery; +pub use discovery::ApiCapabilities; pub mod dynamic; pub use dynamic::{ApiResource, DynamicObject}; @@ -39,14 +40,16 @@ pub mod request; pub use request::Request; mod resource; -pub use resource::{ - ClusterResourceScope, DynamicResourceScope, NamespaceResourceScope, Resource, ResourceExt, ResourceScope, - SubResourceScope, -}; +pub use resource::{Resource, ResourceExt}; pub mod response; pub use response::Status; +mod scope; +pub use scope::{ + ClusterResourceScope, DynamicResourceScope, NamespaceResourceScope, Scope, SubResourceScope, +}; + #[cfg_attr(docsrs, doc(cfg(feature = "schema")))] #[cfg(feature = "schema")] pub mod schema; diff --git a/kube-core/src/object.rs b/kube-core/src/object.rs index ab75b1335..25429035e 100644 --- a/kube-core/src/object.rs +++ b/kube-core/src/object.rs @@ -2,7 +2,8 @@ use crate::{ discovery::ApiResource, metadata::{ListMeta, ObjectMeta, TypeMeta}, - resource::{DynamicResourceScope, Resource}, + resource::Resource, + scope::DynamicResourceScope, }; use serde::{Deserialize, Serialize}; use std::borrow::Cow; @@ -245,6 +246,10 @@ where fn meta_mut(&mut self) -> &mut ObjectMeta { &mut self.metadata } + + fn is_namespaced(dt: &ApiResource) -> bool { + dt.capabilities.namespaced + } } impl HasSpec for Object diff --git a/kube-core/src/resource.rs b/kube-core/src/resource.rs index dcec84578..0cd98836f 100644 --- a/kube-core/src/resource.rs +++ b/kube-core/src/resource.rs @@ -4,14 +4,9 @@ use k8s_openapi::{ apimachinery::pkg::apis::meta::v1::{ManagedFieldsEntry, OwnerReference, Time}, }; +use crate::scope::Scope; use std::{borrow::Cow, collections::BTreeMap}; -pub use k8s_openapi::{ClusterResourceScope, NamespaceResourceScope, ResourceScope, SubResourceScope}; - -/// Indicates that a [`Resource`] is of an indeterminate dynamic scope. -pub struct DynamicResourceScope {} -impl ResourceScope for DynamicResourceScope {} - /// An accessor trait for a kubernetes Resource. /// /// This is for a subset of Kubernetes type that do not end in `List`. @@ -37,7 +32,7 @@ pub trait Resource { /// /// Types from k8s_openapi come with an explicit k8s_openapi::ResourceScope /// Dynamic types should select `Scope = DynamicResourceScope` - type Scope; + type Scope: Scope; /// Returns kind of this object fn kind(dt: &Self::DynamicType) -> Cow<'_, str>; @@ -61,6 +56,12 @@ pub trait Resource { /// This is known as the resource in apimachinery, we rename it for disambiguation. fn plural(dt: &Self::DynamicType) -> Cow<'_, str>; + /// Returns whether the scope is namespaced + /// + /// This will dig into the DynamicType if Scope::is_dynamic + /// otherwise it will defer to Scope::is_namespaced + fn is_namespaced(dt: &Self::DynamicType) -> bool; + /// Creates a url path for http requests for this resource fn url_path(dt: &Self::DynamicType, namespace: Option<&str>) -> String { let n = if let Some(ns) = namespace { @@ -120,6 +121,7 @@ impl Resource for K where K: k8s_openapi::Metadata, K: k8s_openapi::Resource, + S: Scope, { type DynamicType = (); type Scope = S; @@ -151,6 +153,10 @@ where fn meta_mut(&mut self) -> &mut ObjectMeta { self.metadata_mut() } + + fn is_namespaced(_: &()) -> bool { + K::Scope::is_namespaced() + } } /// Helper methods for resources. diff --git a/kube-core/src/scope.rs b/kube-core/src/scope.rs new file mode 100644 index 000000000..c13941b18 --- /dev/null +++ b/kube-core/src/scope.rs @@ -0,0 +1,80 @@ +pub use k8s_openapi::{ClusterResourceScope, NamespaceResourceScope, ResourceScope, SubResourceScope}; + +/// Getters for Scope +/// +/// This allows getting information out of k8s-openapi::ResourceScope +/// without the need for specialization. +/// +/// It also allows us to separate dynamic types from static ones. +pub trait Scope { + /// Whether the Scope is namespaced + fn is_namespaced() -> bool; + /// Whether the Scope is a subresource + fn is_subresource() -> bool; + /// Whether the Scope is an indeteriminate dynamic scope + fn is_dynamic() -> bool; +} + +// extend the ResourceScope traits found in k8s-openapi + +impl Scope for ClusterResourceScope { + fn is_namespaced() -> bool { + false + } + + fn is_subresource() -> bool { + false + } + + fn is_dynamic() -> bool { + false + } +} +impl Scope for NamespaceResourceScope { + fn is_namespaced() -> bool { + true + } + + fn is_subresource() -> bool { + false + } + + fn is_dynamic() -> bool { + false + } +} +impl Scope for SubResourceScope { + fn is_namespaced() -> bool { + false + } + + fn is_subresource() -> bool { + false + } + + fn is_dynamic() -> bool { + true + } +} + +/// Indicates that a [`Resource`] is of an indeterminate dynamic scope. +pub struct DynamicResourceScope {} +impl ResourceScope for DynamicResourceScope {} + +// These implementations checks for namespace/subresource are false here +// because we cannot know the true scope from this struct alone. +// Refer to [`Resource::is_namespaced`] instead, which will inspect the +// DynamicType to find the discovered scope +impl Scope for DynamicResourceScope { + fn is_namespaced() -> bool { + false + } + + fn is_subresource() -> bool { + false + } + + fn is_dynamic() -> bool { + true + } +} diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index 5f250e966..71eb85658 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -320,6 +320,10 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea fn meta_mut(&mut self) -> &mut #k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { &mut self.metadata } + + fn is_namespaced(_: &()) -> bool { + #namespaced + } } }; @@ -452,8 +456,11 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea #crd_meta_name } - fn api_resource() -> #kube_core::dynamic::ApiResource { - #kube_core::dynamic::ApiResource::erase::(&()) + fn api_resource() -> #kube_core::ApiResource { + // TODO: populate subresources + #kube_core::ApiResource::erase::(&()) + .set_shortnames(#shortnames_slice).set_default_verbs() + } fn shortnames() -> &'static [&'static str] { diff --git a/kube-runtime/src/reflector/store.rs b/kube-runtime/src/reflector/store.rs index ac9e2ef9c..66531a365 100644 --- a/kube-runtime/src/reflector/store.rs +++ b/kube-runtime/src/reflector/store.rs @@ -129,7 +129,6 @@ where } } - /// Create a (Reader, Writer) for a `Store` for a typed resource `K` /// /// The `Writer` should be passed to a [`reflector`](crate::reflector()), @@ -145,7 +144,6 @@ where (r, w) } - #[cfg(test)] mod tests { use super::{store, Writer}; diff --git a/kube-runtime/src/utils/mod.rs b/kube-runtime/src/utils/mod.rs index 90d84524e..25f08f5af 100644 --- a/kube-runtime/src/utils/mod.rs +++ b/kube-runtime/src/utils/mod.rs @@ -25,7 +25,6 @@ use std::{ use stream::IntoStream; use tokio::{runtime::Handle, task::JoinHandle}; - /// Allows splitting a `Stream` into several streams that each emit a disjoint subset of the input stream's items, /// like a streaming variant of pattern matching. /// diff --git a/kube/src/lib.rs b/kube/src/lib.rs index 39b6f84c7..3e2c1828a 100644 --- a/kube/src/lib.rs +++ b/kube/src/lib.rs @@ -361,7 +361,7 @@ mod test { async fn derived_resources_discoverable() -> Result<(), Box> { use crate::{ core::{DynamicObject, GroupVersion, GroupVersionKind}, - discovery::{self, verbs, ApiGroup, Discovery, Scope}, + discovery::{self, verbs, ApiGroup, Discovery}, runtime::wait::{await_condition, conditions, Condition}, }; @@ -388,9 +388,9 @@ mod test { // discover by both (recommended kind on groupversion) and (pinned gvk) and they should equal let apigroup = discovery::oneshot::pinned_group(&client, &gv).await?; - let (ar1, caps1) = apigroup.recommended_kind("TestCr").unwrap(); - let (ar2, caps2) = discovery::pinned_kind(&client, &gvk).await?; - assert_eq!(caps1.operations.len(), caps2.operations.len(), "unequal caps"); + let ar1 = apigroup.recommended_kind("TestCr").unwrap(); + let ar2 = discovery::pinned_kind(&client, &gvk).await?; + assert_eq!(ar1.capabilities, ar2.capabilities, "unequal caps"); assert_eq!(ar1, ar2, "unequal apiresource"); assert_eq!(DynamicObject::api_version(&ar2), "kube.rs/v1", "unequal dynver"); @@ -403,7 +403,7 @@ mod test { // check our custom resource first by resolving within groups assert!(discovery.has_group("kube.rs"), "missing group kube.rs"); - let (ar, _caps) = discovery.resolve_gvk(&gvk).unwrap(); + let ar = discovery.resolve_gvk(&gvk).unwrap(); assert_eq!(ar.group, gvk.group, "unexpected discovered group"); assert_eq!(ar.version, gvk.version, "unexcepted discovered ver"); assert_eq!(ar.kind, gvk.kind, "unexpected discovered kind"); @@ -413,11 +413,11 @@ mod test { let firstgroup = groups.next().unwrap(); assert_eq!(firstgroup.name(), ApiGroup::CORE_GROUP, "core not first"); for group in groups { - for (ar, caps) in group.recommended_resources() { - if !caps.supports_operation(verbs::LIST) { + for ar in group.recommended_resources() { + if !ar.supports_operation(verbs::LIST) { continue; } - let api: Api = if caps.scope == Scope::Namespaced { + let api: Api = if ar.namespaced() { Api::default_namespaced_with(client.clone(), &ar) } else { Api::all_with(client.clone(), &ar)