From a342634a7520f68d5266478d4f89c87f753a8d78 Mon Sep 17 00:00:00 2001 From: clux Date: Fri, 30 Sep 2022 23:52:29 +0100 Subject: [PATCH 01/11] Merge `ApiCapabilities` into `ApiResource` Signed-off-by: clux --- examples/dynamic_api.rs | 12 +-- examples/dynamic_watcher.rs | 10 +-- examples/kubectl.rs | 24 +++--- kube-client/src/discovery/apigroup.rs | 102 ++++++++++---------------- kube-client/src/discovery/mod.rs | 12 +-- kube-client/src/discovery/oneshot.rs | 10 +-- kube-client/src/discovery/parse.rs | 46 ++++-------- kube-client/src/error.rs | 4 - kube-core/src/discovery.rs | 63 +++++++++------- kube-core/src/resource.rs | 1 + kube/src/lib.rs | 16 ++-- 11 files changed, 130 insertions(+), 170 deletions(-) diff --git a/examples/dynamic_api.rs b/examples/dynamic_api.rs index 879417528..9e035b99e 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,14 @@ 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..6390cd356 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()); - } else { + if ar.namespaced { info!("saw {} in {}", p.name_any(), p.namespace().unwrap()); + } else { + info!("saw {}", p.name_any()); } } Ok(()) diff --git a/examples/kubectl.rs b/examples/kubectl.rs index 8157fa5e1..4586a415f 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..38afdd095 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(()) @@ -45,7 +45,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()); @@ -121,10 +121,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 +130,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.subresources = parse::find_subresources(&list, &res.name)?; + return Ok(ar); } } Err(Error::Discovery(DiscoveryError::MissingKind(format!( @@ -208,7 +205,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 +221,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 +235,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,8 +248,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.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); @@ -264,12 +261,12 @@ impl ApiGroup { /// } /// ``` /// 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 +274,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 +288,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 +298,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 +311,35 @@ impl ApiGroup { #[cfg(test)] mod tests { - use super::*; + use super::{GroupVersionKind, *}; + #[test] fn test_resources_by_stability() { - let ac = ApiCapabilities { - scope: Scope::Namespaced, - subresources: vec![], - operations: vec![], - }; + let cr_low = GroupVersionKind::gvk("kube.rs", "v1alpha1", "LowCr"); + let testcr_low = ApiResource::from_gvk_with_plural(&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 cr_v1 = GroupVersionKind::gvk("kube.rs", "v1", "TestCr"); + let testcr_v1 = ApiResource::from_gvk_with_plural(&cr_v1, "testcrs"); - 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 cr_v2a1 = GroupVersionKind::gvk("kube.rs", "v2alpha1", "TestCr"); + let testcr_v2alpha1 = ApiResource::from_gvk_with_plural(&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 +349,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..f5690c125 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,14 +87,14 @@ 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); @@ -161,10 +161,10 @@ impl Discovery { /// /// 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..33cb9283a 100644 --- a/kube-client/src/discovery/parse.rs +++ b/kube-client/src/discovery/parse.rs @@ -2,13 +2,11 @@ use crate::{error::DiscoveryError, Error, Result}; use k8s_openapi::apimachinery::pkg::apis::meta::v1::{APIResource, APIResourceList}; use kube_core::{ - discovery::{ApiCapabilities, ApiResource, Scope}, + discovery::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, @@ -21,24 +19,14 @@ pub(crate) fn parse_apiresource( api_version: gv.api_version(), kind: ar.kind.to_string(), plural: ar.name.clone(), + namespaced: ar.namespaced, + verbs: ar.verbs.clone(), + subresources: vec![], }) } -/// 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 +36,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 +47,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 { @@ -77,11 +60,12 @@ impl GroupVersionData { 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.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-core/src/discovery.rs b/kube-core/src/discovery.rs index fba1eba3b..07d7037d0 100644 --- a/kube-core/src/discovery.rs +++ b/kube-core/src/discovery.rs @@ -6,6 +6,9 @@ use serde::{Deserialize, Serialize}; /// /// Enough information to use it like a `Resource` by passing it to the dynamic `Api` /// constructors like `Api::all_with` and `Api::namespaced_with`. +/// +/// Note that this can be constructed in many ways, and all information +/// is only guaranteed to be present through discovery. #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct ApiResource { /// Resource group, empty for core group. @@ -17,12 +20,34 @@ 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, + + /// Whether the resource is namespaced or not + /// + /// Note: only populated through kube-derive and discovery. + pub namespaced: bool, + + /// Supported verbs + /// + /// Note: only populated when constructed through discovery. + pub verbs: Vec, + + /// Supported subresources + /// + /// Note: only populated when constructed through discovery. + /// TODO: populate through kube-derive + pub subresources: Vec, } + impl ApiResource { /// Creates an ApiResource by type-erasing a Resource + /// + /// Note that this variant of constructing an `ApiResource` does not + /// get you verbs and available subresources. + /// If you need this, construct via discovery. pub fn erase(dt: &K::DynamicType) -> Self { ApiResource { group: K::group(dt).to_string(), @@ -30,6 +55,9 @@ impl ApiResource { api_version: K::api_version(dt).to_string(), kind: K::kind(dt).to_string(), plural: K::plural(dt).to_string(), + verbs: vec![], + namespaced: false, + subresources: vec![], } } @@ -41,6 +69,9 @@ impl ApiResource { version: gvk.version.clone(), kind: gvk.kind.clone(), plural: plural.to_string(), + verbs: vec![], + namespaced: false, + subresources: vec![], } } @@ -57,16 +88,7 @@ impl ApiResource { } } -/// Resource scope -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub enum Scope { - /// Objects are global - Cluster, - /// Each object lives in namespace. - Namespaced, -} - -/// Rbac verbs for ApiCapabilities +/// Rbac verbs pub mod verbs { /// Create a resource pub const CREATE: &str = "create"; @@ -86,25 +108,10 @@ pub mod verbs { 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, -} - -impl ApiCapabilities { +impl ApiResource { /// Checks that given verb is supported on this resource. pub fn supports_operation(&self, operation: &str) -> bool { - self.operations.iter().any(|op| op == operation) + self.verbs.iter().any(|op| op == operation) } } diff --git a/kube-core/src/resource.rs b/kube-core/src/resource.rs index dcec84578..50609449f 100644 --- a/kube-core/src/resource.rs +++ b/kube-core/src/resource.rs @@ -153,6 +153,7 @@ where } } + /// Helper methods for resources. pub trait ResourceExt: Resource { /// Deprecated fn equivalent to [`name_unchecked`](ResourceExt::name_unchecked) diff --git a/kube/src/lib.rs b/kube/src/lib.rs index 39b6f84c7..9c3b60ef9 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.verbs.len(), ar2.verbs.len(), "unequal verbs"); 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) From dd5fdf40e26ab68182f39ee053c0fb50e5cb6c47 Mon Sep 17 00:00:00 2001 From: clux Date: Sat, 1 Oct 2022 09:30:50 +0100 Subject: [PATCH 02/11] Add `Scope` to distinguish between scopes Signed-off-by: clux --- kube-client/src/discovery/apigroup.rs | 18 +++---- kube-client/src/discovery/parse.rs | 3 +- kube-core/src/discovery.rs | 28 +++++++--- kube-core/src/dynamic.rs | 5 +- kube-core/src/lib.rs | 10 ++-- kube-core/src/object.rs | 3 +- kube-core/src/resource.rs | 9 ++-- kube-core/src/scope.rs | 76 +++++++++++++++++++++++++++ kube-derive/src/custom_resource.rs | 1 + 9 files changed, 120 insertions(+), 33 deletions(-) create mode 100644 kube-core/src/scope.rs diff --git a/kube-client/src/discovery/apigroup.rs b/kube-client/src/discovery/apigroup.rs index 38afdd095..d9110731d 100644 --- a/kube-client/src/discovery/apigroup.rs +++ b/kube-client/src/discovery/apigroup.rs @@ -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. /// @@ -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 @@ -311,18 +307,18 @@ impl ApiGroup { #[cfg(test)] mod tests { - use super::{GroupVersionKind, *}; + use super::{GroupVersionKind as GVK, *}; #[test] fn test_resources_by_stability() { - let cr_low = GroupVersionKind::gvk("kube.rs", "v1alpha1", "LowCr"); + let cr_low = GVK::gvk("kube.rs", "v1alpha1", "LowCr"); let testcr_low = ApiResource::from_gvk_with_plural(&cr_low, "lowcrs"); - let cr_v1 = GroupVersionKind::gvk("kube.rs", "v1", "TestCr"); + let cr_v1 = GVK::gvk("kube.rs", "v1", "TestCr"); let testcr_v1 = ApiResource::from_gvk_with_plural(&cr_v1, "testcrs"); - let cr_v2a1 = GroupVersionKind::gvk("kube.rs", "v2alpha1", "TestCr"); + let cr_v2a1 = GVK::gvk("kube.rs", "v2alpha1", "TestCr"); let testcr_v2alpha1 = ApiResource::from_gvk_with_plural(&cr_v2a1, "testcrs"); let group = ApiGroup { diff --git a/kube-client/src/discovery/parse.rs b/kube-client/src/discovery/parse.rs index 33cb9283a..d58d5e7d7 100644 --- a/kube-client/src/discovery/parse.rs +++ b/kube-client/src/discovery/parse.rs @@ -21,6 +21,7 @@ pub(crate) fn parse_apiresource( plural: ar.name.clone(), namespaced: ar.namespaced, verbs: ar.verbs.clone(), + shortnames: ar.short_names.clone().unwrap_or_default(), subresources: vec![], }) } @@ -55,7 +56,7 @@ 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; } diff --git a/kube-core/src/discovery.rs b/kube-core/src/discovery.rs index 07d7037d0..e5ef47219 100644 --- a/kube-core/src/discovery.rs +++ b/kube-core/src/discovery.rs @@ -1,5 +1,5 @@ //! Type information structs for API discovery -use crate::{gvk::GroupVersionKind, resource::Resource}; +use crate::{gvk::GroupVersionKind, resource::Resource, scope::Scope}; use serde::{Deserialize, Serialize}; /// Information about a Kubernetes API resource @@ -34,6 +34,11 @@ pub struct ApiResource { /// Note: only populated when constructed through discovery. pub verbs: Vec, + /// Supported shortnames + /// + /// Note: only populated when constructed through discovery or kube-derive. + pub shortnames: Vec, + /// Supported subresources /// /// Note: only populated when constructed through discovery. @@ -41,7 +46,6 @@ pub struct ApiResource { pub subresources: Vec, } - impl ApiResource { /// Creates an ApiResource by type-erasing a Resource /// @@ -55,13 +59,17 @@ impl ApiResource { api_version: K::api_version(dt).to_string(), kind: K::kind(dt).to_string(), plural: K::plural(dt).to_string(), + namespaced: ::Scope::is_namespaced(), + // discovery/derive-only properties left blank verbs: vec![], - namespaced: false, subresources: vec![], + shortnames: vec![], } } - /// Creates an ApiResource from group, version, kind and plural name. + /// Creates a minimal ApiResource from group, version, kind and plural name + /// + /// This is the minimal `ApiVersion` needed to communicate pub fn from_gvk_with_plural(gvk: &GroupVersionKind, plural: &str) -> Self { ApiResource { api_version: gvk.api_version(), @@ -69,13 +77,15 @@ impl ApiResource { version: gvk.version.clone(), kind: gvk.kind.clone(), plural: plural.to_string(), + // discovery/derive-only properties left blank + namespaced: false, // TODO: force users to set this? verbs: vec![], - namespaced: false, subresources: vec![], + shortnames: vec![], } } - /// Creates an ApiResource from group, version and kind. + /// Infer a minimal ApiResource from group, version and kind /// /// # Warning /// This function will **guess** the resource plural name. @@ -86,6 +96,12 @@ impl ApiResource { pub fn from_gvk(gvk: &GroupVersionKind) -> Self { ApiResource::from_gvk_with_plural(gvk, &to_plural(&gvk.kind.to_ascii_lowercase())) } + + /// Set the shortnames of an ApiResource + pub fn shortnames(mut self, shortnames: &[&'static str]) -> Self { + self.shortnames = shortnames.iter().map(|x| x.to_string()).collect(); + self + } } /// Rbac verbs diff --git a/kube-core/src/dynamic.rs b/kube-core/src/dynamic.rs index 885b32e40..d61a5cffe 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; diff --git a/kube-core/src/lib.rs b/kube-core/src/lib.rs index 71b81b917..a34d4a991 100644 --- a/kube-core/src/lib.rs +++ b/kube-core/src/lib.rs @@ -39,14 +39,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..907480224 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; diff --git a/kube-core/src/resource.rs b/kube-core/src/resource.rs index 50609449f..bd7d024d0 100644 --- a/kube-core/src/resource.rs +++ b/kube-core/src/resource.rs @@ -4,13 +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. /// @@ -37,7 +33,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>; @@ -120,6 +116,7 @@ impl Resource for K where K: k8s_openapi::Metadata, K: k8s_openapi::Resource, + S: Scope, { type DynamicType = (); type Scope = S; diff --git a/kube-core/src/scope.rs b/kube-core/src/scope.rs new file mode 100644 index 000000000..127731d04 --- /dev/null +++ b/kube-core/src/scope.rs @@ -0,0 +1,76 @@ +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 {} + +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..9ebd7aeb4 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -454,6 +454,7 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea fn api_resource() -> #kube_core::dynamic::ApiResource { #kube_core::dynamic::ApiResource::erase::(&()) + .shortnames(#shortnames_slice) } fn shortnames() -> &'static [&'static str] { From 9d8c22153b2a0e93caba04a0fea11612fb4b507d Mon Sep 17 00:00:00 2001 From: clux Date: Sat, 1 Oct 2022 12:05:27 +0100 Subject: [PATCH 03/11] re-classify ApiResource constructors Signed-off-by: clux --- kube-client/src/discovery/apigroup.rs | 6 +-- kube-core/src/discovery.rs | 69 +++++++++++++++++++-------- kube-derive/src/custom_resource.rs | 2 + 3 files changed, 53 insertions(+), 24 deletions(-) diff --git a/kube-client/src/discovery/apigroup.rs b/kube-client/src/discovery/apigroup.rs index d9110731d..33e9d4750 100644 --- a/kube-client/src/discovery/apigroup.rs +++ b/kube-client/src/discovery/apigroup.rs @@ -313,13 +313,13 @@ mod tests { #[test] fn test_resources_by_stability() { let cr_low = GVK::gvk("kube.rs", "v1alpha1", "LowCr"); - let testcr_low = ApiResource::from_gvk_with_plural(&cr_low, "lowcrs"); + let testcr_low = ApiResource::new(&cr_low, "lowcrs", true); let cr_v1 = GVK::gvk("kube.rs", "v1", "TestCr"); - let testcr_v1 = ApiResource::from_gvk_with_plural(&cr_v1, "testcrs"); + let testcr_v1 = ApiResource::new(&cr_v1, "testcrs", true); let cr_v2a1 = GVK::gvk("kube.rs", "v2alpha1", "TestCr"); - let testcr_v2alpha1 = ApiResource::from_gvk_with_plural(&cr_v2a1, "testcrs"); + let testcr_v2alpha1 = ApiResource::new(&cr_v2a1, "testcrs", true); let group = ApiGroup { name: "kube.rs".into(), diff --git a/kube-core/src/discovery.rs b/kube-core/src/discovery.rs index e5ef47219..b52a7dbf2 100644 --- a/kube-core/src/discovery.rs +++ b/kube-core/src/discovery.rs @@ -4,11 +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`. /// -/// Note that this can be constructed in many ways, and all information -/// is only guaranteed to be present through discovery. +/// 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 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. @@ -20,18 +35,14 @@ pub struct ApiResource { pub api_version: String, /// Singular PascalCase name of the resource pub kind: String, - /// Resource name / plural name pub plural: String, - /// Whether the resource is namespaced or not - /// - /// Note: only populated through kube-derive and discovery. pub namespaced: bool, /// Supported verbs /// - /// Note: only populated when constructed through discovery. + /// Note: only populated when constructed through discovery or kube-derive pub verbs: Vec, /// Supported shortnames @@ -42,7 +53,6 @@ pub struct ApiResource { /// Supported subresources /// /// Note: only populated when constructed through discovery. - /// TODO: populate through kube-derive pub subresources: Vec, } @@ -67,41 +77,54 @@ impl ApiResource { } } - /// Creates a minimal ApiResource from group, version, kind and plural name + /// Creates a new ApiResource from a GVK, plural and a namespaced bool /// - /// This is the minimal `ApiVersion` needed to communicate - pub fn from_gvk_with_plural(gvk: &GroupVersionKind, plural: &str) -> Self { + /// This is the **minimal** variant needed to use with the dynamic api + /// It does not contain information abut verbs, subresources and shortnames. + pub fn new(gvk: &GroupVersionKind, plural: &str, namespaced: bool) -> Self { ApiResource { api_version: gvk.api_version(), group: gvk.group.clone(), version: gvk.version.clone(), kind: gvk.kind.clone(), plural: plural.to_string(), - // discovery/derive-only properties left blank - namespaced: false, // TODO: force users to set this? + namespaced: namespaced, + // non-essential properties left blank verbs: vec![], subresources: vec![], shortnames: vec![], } } - /// Infer a minimal ApiResource from group, version and kind + /// Infer a minimal ApiResource from a GVK and whether it's namespaced /// /// # 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) + /// Otherwise consider using [`ApiResource::new`](crate::discovery::ApiResource::from_gvk_with_plural) /// to explicitly set the plural, or run api discovery on it via `kube::discovery`. - pub fn from_gvk(gvk: &GroupVersionKind) -> Self { - ApiResource::from_gvk_with_plural(gvk, &to_plural(&gvk.kind.to_ascii_lowercase())) + pub fn from_gvk(gvk: &GroupVersionKind, namespaced: bool) -> Self { + ApiResource::new(gvk, &to_plural(&gvk.kind.to_ascii_lowercase()), namespaced) } - /// Set the shortnames of an ApiResource - pub fn shortnames(mut self, shortnames: &[&'static str]) -> Self { + /// Set the shortnames + pub fn shortnames(mut self, shortnames: &[&str]) -> Self { self.shortnames = shortnames.iter().map(|x| x.to_string()).collect(); self } + + /// Set the allowed verbs + pub fn verbs(mut self, verbs: &[&str]) -> Self { + self.verbs = verbs.iter().map(|x| x.to_string()).collect(); + self + } + + /// Set the default verbs + pub fn default_verbs(mut self) -> Self { + self.verbs = verbs::DEFAULT_VERBS.iter().map(|x| x.to_string()).collect(); + self + } } /// Rbac verbs @@ -122,6 +145,10 @@ pub mod verbs { pub const UPDATE: &str = "update"; /// Patch an object pub const PATCH: &str = "patch"; + + /// All the default verbs + pub const DEFAULT_VERBS: &[&str; 8] = + &[CREATE, GET, LIST, WATCH, DELETE, DELETE_COLLECTION, UPDATE, PATCH]; } impl ApiResource { diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index 9ebd7aeb4..5099e461a 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -455,6 +455,8 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea fn api_resource() -> #kube_core::dynamic::ApiResource { #kube_core::dynamic::ApiResource::erase::(&()) .shortnames(#shortnames_slice) + .default_verbs() + // TODO: populate subresources } fn shortnames() -> &'static [&'static str] { From 9d26b2a14491e102aba53bede023956287c85930 Mon Sep 17 00:00:00 2001 From: clux Date: Sat, 1 Oct 2022 12:08:59 +0100 Subject: [PATCH 04/11] last reference to ApiCapabilities Signed-off-by: clux --- kube-client/src/discovery/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kube-client/src/discovery/mod.rs b/kube-client/src/discovery/mod.rs index f5690c125..84a1102f6 100644 --- a/kube-client/src/discovery/mod.rs +++ b/kube-client/src/discovery/mod.rs @@ -157,7 +157,7 @@ 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). From 66db2e3c23396b87f3f9cccef3b9eb6f4d2b174d Mon Sep 17 00:00:00 2001 From: clux Date: Sat, 1 Oct 2022 12:15:59 +0100 Subject: [PATCH 05/11] avoid breaking both ApiResource ctors Signed-off-by: clux --- kube-core/src/discovery.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/kube-core/src/discovery.rs b/kube-core/src/discovery.rs index b52a7dbf2..35f855b46 100644 --- a/kube-core/src/discovery.rs +++ b/kube-core/src/discovery.rs @@ -96,16 +96,23 @@ impl ApiResource { } } - /// Infer a minimal ApiResource from a GVK and whether it's namespaced + /// Infer a minimal ApiResource from a GVK as cluster scoped /// /// # Warning - /// This function will **guess** the resource plural name. - /// Usually, this is ok, but for CRDs with complex pluralisations it can fail. + /// This function will **guess** the resource plural name which can fail + /// for CRDs with complex pluralisations it can fail. It will also assume cluster scope. + /// /// If you are getting your values from `kube_derive` use the generated method for giving you an [`ApiResource`]. - /// Otherwise consider using [`ApiResource::new`](crate::discovery::ApiResource::from_gvk_with_plural) - /// to explicitly set the plural, or run api discovery on it via `kube::discovery`. - pub fn from_gvk(gvk: &GroupVersionKind, namespaced: bool) -> Self { - ApiResource::new(gvk, &to_plural(&gvk.kind.to_ascii_lowercase()), namespaced) + /// Otherwise consider using [`ApiResource::new`](crate::discovery::ApiResource::new) + /// to explicitly set the plural and scope, or run api discovery on it via `kube::discovery`. + pub fn from_gvk(gvk: &GroupVersionKind) -> Self { + ApiResource::new(gvk, &to_plural(&gvk.kind.to_ascii_lowercase()), false) + } + + /// Set the whether the resource is namsepace scoped + pub fn namespaced(mut self, namespaced: bool) -> Self { + self.namespaced = namespaced; + self } /// Set the shortnames From 6046a118ca13eb142b26c3823bd65768630a1c0a Mon Sep 17 00:00:00 2001 From: Eirik A Date: Mon, 3 Oct 2022 22:14:02 +0100 Subject: [PATCH 06/11] Update examples/dynamic_watcher.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Teo Klestrup Röijezon Signed-off-by: Eirik A --- examples/dynamic_watcher.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/dynamic_watcher.rs b/examples/dynamic_watcher.rs index 6390cd356..2f3f60ddb 100644 --- a/examples/dynamic_watcher.rs +++ b/examples/dynamic_watcher.rs @@ -30,8 +30,8 @@ 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 ar.namespaced { - info!("saw {} in {}", p.name_any(), p.namespace().unwrap()); + if let Some(ns) = p.namespace() { + info!("saw {} in {}", p.name_any(), ns); } else { info!("saw {}", p.name_any()); } From ac5b93e825ca07f2458949cea1c0223eaa0cab53 Mon Sep 17 00:00:00 2001 From: clux Date: Mon, 3 Oct 2022 22:14:51 +0100 Subject: [PATCH 07/11] minor clippy + typo + capitalization Signed-off-by: clux --- kube-core/src/discovery.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kube-core/src/discovery.rs b/kube-core/src/discovery.rs index 35f855b46..2846f216a 100644 --- a/kube-core/src/discovery.rs +++ b/kube-core/src/discovery.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; /// - `discovery` module in kube/kube-client /// - `CustomResource` derive in kube-derive /// -/// Will have ALL the extraneous data about shortnames, verbs, and resources. +/// will have ALL the extraneous data about shortnames, verbs, and resources. /// /// # Warning /// @@ -23,7 +23,7 @@ use serde::{Deserialize, Serialize}; /// 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 fail to query. Provide accurate `plural` and `namespaced` data to be safe. +/// 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. @@ -88,7 +88,7 @@ impl ApiResource { version: gvk.version.clone(), kind: gvk.kind.clone(), plural: plural.to_string(), - namespaced: namespaced, + namespaced, // non-essential properties left blank verbs: vec![], subresources: vec![], From f767bb1a5d5e18c68fccd3f912abd6004935a803 Mon Sep 17 00:00:00 2001 From: clux Date: Wed, 5 Oct 2022 15:17:24 +0100 Subject: [PATCH 08/11] fmt newline Signed-off-by: clux --- kube-client/src/discovery/apigroup.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/kube-client/src/discovery/apigroup.rs b/kube-client/src/discovery/apigroup.rs index 33e9d4750..8f0876d5f 100644 --- a/kube-client/src/discovery/apigroup.rs +++ b/kube-client/src/discovery/apigroup.rs @@ -335,7 +335,6 @@ mod tests { GroupVersionData { version: "v2alpha1".into(), resources: vec![testcr_v2alpha1], - }, ], preferred: Some(String::from("v1")), From 24840accc4a826d05370261ed528c008a377e3e9 Mon Sep 17 00:00:00 2001 From: clux Date: Thu, 6 Oct 2022 22:46:20 +0100 Subject: [PATCH 09/11] Re-add ApiCapabilities in limited form Signed-off-by: clux --- examples/crd_derive_multi.rs | 2 - examples/dynamic_api.rs | 4 +- kube-client/src/discovery/apigroup.rs | 17 ++-- kube-client/src/discovery/mod.rs | 4 +- kube-client/src/discovery/parse.rs | 15 ++- kube-client/src/lib.rs | 1 - kube-core/src/crd.rs | 1 - kube-core/src/discovery.rs | 130 +++++++++++++++----------- kube-core/src/dynamic.rs | 4 + kube-core/src/lib.rs | 1 + kube-core/src/object.rs | 4 + kube-core/src/resource.rs | 12 ++- kube-core/src/scope.rs | 4 + kube-derive/src/custom_resource.rs | 13 ++- kube-runtime/src/reflector/store.rs | 2 - kube-runtime/src/utils/mod.rs | 1 - kube/src/lib.rs | 5 +- 17 files changed, 137 insertions(+), 83 deletions(-) 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 9e035b99e..8aef67af1 100644 --- a/examples/dynamic_api.rs +++ b/examples/dynamic_api.rs @@ -15,9 +15,11 @@ async fn main() -> anyhow::Result<()> { let discovery = Discovery::new(client.clone()).run().await?; for group in discovery.groups() { for ar in group.recommended_resources() { - if !ar.supports_operation(verbs::LIST) { + let caps = ar.capabilities.as_ref().unwrap(); + if !caps.supports_operation(verbs::LIST) { continue; } + let api: Api = if ar.namespaced { Api::default_namespaced_with(client.clone(), &ar) } else { diff --git a/kube-client/src/discovery/apigroup.rs b/kube-client/src/discovery/apigroup.rs index 8f0876d5f..d9c929eba 100644 --- a/kube-client/src/discovery/apigroup.rs +++ b/kube-client/src/discovery/apigroup.rs @@ -129,7 +129,9 @@ impl ApiGroup { let mut ar = parse::parse_apiresource(res, &list.group_version).map_err( |ParseGroupVersionError(s)| Error::Discovery(DiscoveryError::InvalidGroupVersion(s)), )?; - ar.subresources = parse::find_subresources(&list, &res.name)?; + if let Some(caps) = &mut ar.capabilities { + caps.subresources = parse::find_subresources(&list, &res.name)?; + } return Ok(ar); } } @@ -218,7 +220,8 @@ impl ApiGroup { /// let client = Client::try_default().await?; /// let apigroup = discovery::group(&client, "apiregistration.k8s.io").await?; /// for ar in apigroup.recommended_resources() { - /// if !ar.supports_operation(verbs::LIST) { + /// let caps = ar.capabilities.as_ref().unwrap(); + /// if !caps.supports_operation(verbs::LIST) { /// continue; /// } /// let api: Api = Api::all_with(client.clone(), &ar); @@ -245,12 +248,12 @@ impl ApiGroup { /// let client = Client::try_default().await?; /// let apigroup = discovery::group(&client, "apiregistration.k8s.io").await?; /// for ar in apigroup.resources_by_stability() { - /// if !ar.supports_operation(verbs::LIST) { + /// if !ar.supports_operation(verbs::LIST).unwrap() { /// 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(()) @@ -313,13 +316,13 @@ mod tests { #[test] fn test_resources_by_stability() { let cr_low = GVK::gvk("kube.rs", "v1alpha1", "LowCr"); - let testcr_low = ApiResource::new(&cr_low, "lowcrs", true); + let testcr_low = ApiResource::new(&cr_low, "lowcrs"); let cr_v1 = GVK::gvk("kube.rs", "v1", "TestCr"); - let testcr_v1 = ApiResource::new(&cr_v1, "testcrs", true); + let testcr_v1 = ApiResource::new(&cr_v1, "testcrs"); let cr_v2a1 = GVK::gvk("kube.rs", "v2alpha1", "TestCr"); - let testcr_v2alpha1 = ApiResource::new(&cr_v2a1, "testcrs", true); + let testcr_v2alpha1 = ApiResource::new(&cr_v2a1, "testcrs"); let group = ApiGroup { name: "kube.rs".into(), diff --git a/kube-client/src/discovery/mod.rs b/kube-client/src/discovery/mod.rs index 84a1102f6..9626d8471 100644 --- a/kube-client/src/discovery/mod.rs +++ b/kube-client/src/discovery/mod.rs @@ -94,13 +94,13 @@ impl Discovery { /// let discovery = Discovery::new(client.clone()).run().await?; /// for group in discovery.groups() { /// for ar in group.recommended_resources() { - /// if !ar.supports_operation(verbs::LIST) { + /// if !ar.supports_operation(verbs::LIST).unwrap() { /// 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()); /// } /// } /// } diff --git a/kube-client/src/discovery/parse.rs b/kube-client/src/discovery/parse.rs index d58d5e7d7..e5372ed16 100644 --- a/kube-client/src/discovery/parse.rs +++ b/kube-client/src/discovery/parse.rs @@ -2,7 +2,7 @@ use crate::{error::DiscoveryError, Error, Result}; use k8s_openapi::apimachinery::pkg::apis::meta::v1::{APIResource, APIResourceList}; use kube_core::{ - discovery::ApiResource, + discovery::{ApiCapabilities, ApiResource}, gvk::{GroupVersion, ParseGroupVersionError}, }; @@ -12,6 +12,11 @@ pub(crate) fn parse_apiresource( group_version: &str, ) -> Result { let gv: GroupVersion = group_version.parse()?; + let caps = ApiCapabilities { + 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()), @@ -20,9 +25,7 @@ pub(crate) fn parse_apiresource( kind: ar.kind.to_string(), plural: ar.name.clone(), namespaced: ar.namespaced, - verbs: ar.verbs.clone(), - shortnames: ar.short_names.clone().unwrap_or_default(), - subresources: vec![], + capabilities: Some(caps), }) } @@ -65,7 +68,9 @@ impl GroupVersionData { parse_apiresource(res, &list.group_version).map_err(|ParseGroupVersionError(s)| { Error::Discovery(DiscoveryError::InvalidGroupVersion(s)) })?; - ar.subresources = find_subresources(&list, &res.name)?; + if let Some(caps) = &mut ar.capabilities { + caps.subresources = find_subresources(&list, &res.name)?; + } resources.push(ar); } Ok(GroupVersionData { version, resources }) 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 2846f216a..a37b9bf4d 100644 --- a/kube-core/src/discovery.rs +++ b/kube-core/src/discovery.rs @@ -1,5 +1,5 @@ //! Type information structs for API discovery -use crate::{gvk::GroupVersionKind, resource::Resource, scope::Scope}; +use crate::{gvk::GroupVersionKind, resource::Resource}; use serde::{Deserialize, Serialize}; /// Information about a Kubernetes API resource @@ -37,23 +37,11 @@ pub struct ApiResource { pub kind: String, /// Resource name / plural name pub plural: String, - /// Whether the resource is namespaced or not + /// Whether the resource is namespaced pub namespaced: bool, - /// Supported verbs - /// - /// Note: only populated when constructed through discovery or kube-derive - pub verbs: Vec, - - /// Supported shortnames - /// - /// Note: only populated when constructed through discovery or kube-derive. - pub shortnames: Vec, - - /// Supported subresources - /// - /// Note: only populated when constructed through discovery. - pub subresources: Vec, + /// Capabilities from kube-derive or api discovery + pub capabilities: Option, } impl ApiResource { @@ -63,75 +51,79 @@ impl ApiResource { /// get you verbs and available subresources. /// If you need this, construct via discovery. pub fn erase(dt: &K::DynamicType) -> Self { + // TODO: if Scope::is_dynamic() we could have the scope 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(), - namespaced: ::Scope::is_namespaced(), - // discovery/derive-only properties left blank - verbs: vec![], - subresources: vec![], - shortnames: vec![], + namespaced: K::is_namespaced(dt), + capabilities: None, } } - /// Creates a new ApiResource from a GVK, plural and a namespaced bool + /// Creates a new ApiResource from a GVK and a plural name /// - /// This is the **minimal** variant needed to use with the dynamic api - /// It does not contain information abut verbs, subresources and shortnames. - pub fn new(gvk: &GroupVersionKind, plural: &str, namespaced: bool) -> Self { + /// 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(), - namespaced, - // non-essential properties left blank - verbs: vec![], - subresources: vec![], - shortnames: vec![], + namespaced: false, + capabilities: None, } } - /// Infer a minimal ApiResource from a GVK as cluster scoped + /// 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 is a convenience utility intended for quick experiments. /// This function will **guess** the resource plural name which can fail - /// for CRDs with complex pluralisations it can fail. It will also assume cluster scope. + /// for CRDs with complex pluralisations. /// - /// If you are getting your values from `kube_derive` use the generated method for giving you an [`ApiResource`]. - /// Otherwise consider using [`ApiResource::new`](crate::discovery::ApiResource::new) - /// to explicitly set the plural and scope, or run api discovery on it via `kube::discovery`. + /// Consider using [`ApiResource::new`](crate::discovery::ApiResource::new) + /// to explicitly set the plural instead. pub fn from_gvk(gvk: &GroupVersionKind) -> Self { - ApiResource::new(gvk, &to_plural(&gvk.kind.to_ascii_lowercase()), false) - } - - /// Set the whether the resource is namsepace scoped - pub fn namespaced(mut self, namespaced: bool) -> Self { - self.namespaced = namespaced; - self + ApiResource::new(gvk, &to_plural(&gvk.kind.to_ascii_lowercase())) } - /// Set the shortnames - pub fn shortnames(mut self, shortnames: &[&str]) -> Self { - self.shortnames = shortnames.iter().map(|x| x.to_string()).collect(); + /// Attach capabilities to a manually constructed [`ApiResource`] + pub fn with_caps(mut self, caps: ApiCapabilities) -> Self { + self.capabilities = Some(caps); self } - /// Set the allowed verbs - pub fn verbs(mut self, verbs: &[&str]) -> Self { - self.verbs = verbs.iter().map(|x| x.to_string()).collect(); + /// Set the whether the resource is namsepace scoped + pub fn namespaced(mut self, namespaced: bool) -> Self { + self.namespaced = namespaced; self } +} - /// Set the default verbs - pub fn default_verbs(mut self) -> Self { - self.verbs = verbs::DEFAULT_VERBS.iter().map(|x| x.to_string()).collect(); - self - } +/// 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 { + /// Supported verbs that are queryable + pub verbs: Vec, + /// Supported shortnames + pub shortnames: Vec, + /// Supported subresources + pub subresources: Vec, } /// Rbac verbs @@ -159,6 +151,38 @@ pub mod verbs { } impl ApiResource { + /// Checks that given verb is supported on this resource. + /// + /// If verbs are missing, we return a None to indicate + /// that we do not have enough information to say true or false. + pub fn supports_operation(&self, operation: &str) -> Option { + if let Some(caps) = &self.capabilities { + Some(caps.verbs.iter().any(|op| op == operation)) + } else { + None + } + } +} + +impl ApiCapabilities { + /// Set the shortnames + pub fn shortnames(mut self, shortnames: &[&str]) -> Self { + self.shortnames = shortnames.iter().map(|x| x.to_string()).collect(); + self + } + + /// Set the allowed verbs + pub fn verbs(mut self, verbs: &[&str]) -> Self { + self.verbs = verbs.iter().map(|x| x.to_string()).collect(); + self + } + + /// Set the default verbs + pub fn default_verbs(mut self) -> Self { + self.verbs = verbs::DEFAULT_VERBS.iter().map(|x| x.to_string()).collect(); + self + } + /// Checks that given verb is supported on this resource. pub fn supports_operation(&self, operation: &str) -> bool { self.verbs.iter().any(|op| op == operation) diff --git a/kube-core/src/dynamic.rs b/kube-core/src/dynamic.rs index d61a5cffe..6c94b97b2 100644 --- a/kube-core/src/dynamic.rs +++ b/kube-core/src/dynamic.rs @@ -87,6 +87,10 @@ impl Resource for DynamicObject { fn meta_mut(&mut self) -> &mut ObjectMeta { &mut self.metadata } + + fn is_namespaced(dt: &ApiResource) -> bool { + dt.namespaced + } } #[cfg(test)] diff --git a/kube-core/src/lib.rs b/kube-core/src/lib.rs index a34d4a991..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}; diff --git a/kube-core/src/object.rs b/kube-core/src/object.rs index 907480224..1476968c4 100644 --- a/kube-core/src/object.rs +++ b/kube-core/src/object.rs @@ -246,6 +246,10 @@ where fn meta_mut(&mut self) -> &mut ObjectMeta { &mut self.metadata } + + fn is_namespaced(dt: &ApiResource) -> bool { + dt.namespaced + } } impl HasSpec for Object diff --git a/kube-core/src/resource.rs b/kube-core/src/resource.rs index bd7d024d0..0cd98836f 100644 --- a/kube-core/src/resource.rs +++ b/kube-core/src/resource.rs @@ -7,7 +7,6 @@ use k8s_openapi::{ use crate::scope::Scope; use std::{borrow::Cow, collections::BTreeMap}; - /// An accessor trait for a kubernetes Resource. /// /// This is for a subset of Kubernetes type that do not end in `List`. @@ -57,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 { @@ -148,8 +153,11 @@ where fn meta_mut(&mut self) -> &mut ObjectMeta { self.metadata_mut() } -} + fn is_namespaced(_: &()) -> bool { + K::Scope::is_namespaced() + } +} /// Helper methods for resources. pub trait ResourceExt: Resource { diff --git a/kube-core/src/scope.rs b/kube-core/src/scope.rs index 127731d04..c13941b18 100644 --- a/kube-core/src/scope.rs +++ b/kube-core/src/scope.rs @@ -61,6 +61,10 @@ impl Scope for SubResourceScope { 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 diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index 5099e461a..43177a6d5 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,11 +456,12 @@ 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::(&()) - .shortnames(#shortnames_slice) - .default_verbs() + fn api_resource() -> #kube_core::ApiResource { + let caps = #kube_core::ApiCapabilities::default().shortnames(#shortnames_slice).default_verbs(); // TODO: populate subresources + #kube_core::ApiResource::erase::(&()) + .with_caps(caps) + } 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 9c3b60ef9..a801278c9 100644 --- a/kube/src/lib.rs +++ b/kube/src/lib.rs @@ -390,7 +390,7 @@ mod test { let apigroup = discovery::oneshot::pinned_group(&client, &gv).await?; let ar1 = apigroup.recommended_kind("TestCr").unwrap(); let ar2 = discovery::pinned_kind(&client, &gvk).await?; - assert_eq!(ar1.verbs.len(), ar2.verbs.len(), "unequal verbs"); + 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"); @@ -414,7 +414,8 @@ mod test { assert_eq!(firstgroup.name(), ApiGroup::CORE_GROUP, "core not first"); for group in groups { for ar in group.recommended_resources() { - if !ar.supports_operation(verbs::LIST) { + let caps = ar.capabilities.as_ref().unwrap(); + if !caps.supports_operation(verbs::LIST) { continue; } let api: Api = if ar.namespaced { From 7219659b2f11b619c32f5d91d89ad0c83ed089de Mon Sep 17 00:00:00 2001 From: clux Date: Thu, 6 Oct 2022 22:47:44 +0100 Subject: [PATCH 10/11] leftin todo Signed-off-by: clux --- kube-core/src/discovery.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/kube-core/src/discovery.rs b/kube-core/src/discovery.rs index a37b9bf4d..aa7f949a4 100644 --- a/kube-core/src/discovery.rs +++ b/kube-core/src/discovery.rs @@ -51,7 +51,6 @@ impl ApiResource { /// get you verbs and available subresources. /// If you need this, construct via discovery. pub fn erase(dt: &K::DynamicType) -> Self { - // TODO: if Scope::is_dynamic() we could have the scope ApiResource { group: K::group(dt).to_string(), version: K::version(dt).to_string(), From 39f81166c37147289727183a4ced92a6a9802ac3 Mon Sep 17 00:00:00 2001 From: clux Date: Fri, 7 Oct 2022 07:14:51 +0100 Subject: [PATCH 11/11] simplify dynamic interface and put namespaced inside capabilities Signed-off-by: clux --- examples/dynamic_api.rs | 5 +- examples/kubectl.rs | 2 +- kube-client/src/discovery/apigroup.rs | 9 +-- kube-client/src/discovery/mod.rs | 2 +- kube-client/src/discovery/parse.rs | 8 +-- kube-core/src/discovery.rs | 86 ++++++++++++--------------- kube-core/src/dynamic.rs | 2 +- kube-core/src/object.rs | 2 +- kube-derive/src/custom_resource.rs | 3 +- kube/src/lib.rs | 5 +- 10 files changed, 54 insertions(+), 70 deletions(-) diff --git a/examples/dynamic_api.rs b/examples/dynamic_api.rs index 8aef67af1..e7111cca9 100644 --- a/examples/dynamic_api.rs +++ b/examples/dynamic_api.rs @@ -15,12 +15,11 @@ async fn main() -> anyhow::Result<()> { let discovery = Discovery::new(client.clone()).run().await?; for group in discovery.groups() { for ar in group.recommended_resources() { - let caps = ar.capabilities.as_ref().unwrap(); - if !caps.supports_operation(verbs::LIST) { + if !ar.supports_operation(verbs::LIST) { continue; } - let api: Api = if ar.namespaced { + let api: Api = if ar.namespaced() { Api::default_namespaced_with(client.clone(), &ar) } else { Api::all_with(client.clone(), &ar) diff --git a/examples/kubectl.rs b/examples/kubectl.rs index 4586a415f..66fc5e824 100644 --- a/examples/kubectl.rs +++ b/examples/kubectl.rs @@ -218,7 +218,7 @@ async fn main() -> Result<()> { } fn dynamic_api(ar: ApiResource, client: Client, ns: &Option, all: bool) -> Api { - if !ar.namespaced || all { + 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 d9c929eba..da0ef0050 100644 --- a/kube-client/src/discovery/apigroup.rs +++ b/kube-client/src/discovery/apigroup.rs @@ -129,9 +129,7 @@ impl ApiGroup { let mut ar = parse::parse_apiresource(res, &list.group_version).map_err( |ParseGroupVersionError(s)| Error::Discovery(DiscoveryError::InvalidGroupVersion(s)), )?; - if let Some(caps) = &mut ar.capabilities { - caps.subresources = parse::find_subresources(&list, &res.name)?; - } + ar.capabilities.subresources = parse::find_subresources(&list, &res.name)?; return Ok(ar); } } @@ -220,8 +218,7 @@ impl ApiGroup { /// let client = Client::try_default().await?; /// let apigroup = discovery::group(&client, "apiregistration.k8s.io").await?; /// for ar in apigroup.recommended_resources() { - /// let caps = ar.capabilities.as_ref().unwrap(); - /// if !caps.supports_operation(verbs::LIST) { + /// if !ar.supports_operation(verbs::LIST) { /// continue; /// } /// let api: Api = Api::all_with(client.clone(), &ar); @@ -248,7 +245,7 @@ impl ApiGroup { /// let client = Client::try_default().await?; /// let apigroup = discovery::group(&client, "apiregistration.k8s.io").await?; /// for ar in apigroup.resources_by_stability() { - /// if !ar.supports_operation(verbs::LIST).unwrap() { + /// if !ar.supports_operation(verbs::LIST) { /// continue; /// } /// let api: Api = Api::all_with(client.clone(), &ar); diff --git a/kube-client/src/discovery/mod.rs b/kube-client/src/discovery/mod.rs index 9626d8471..fdb0992fc 100644 --- a/kube-client/src/discovery/mod.rs +++ b/kube-client/src/discovery/mod.rs @@ -94,7 +94,7 @@ impl Discovery { /// let discovery = Discovery::new(client.clone()).run().await?; /// for group in discovery.groups() { /// for ar in group.recommended_resources() { - /// if !ar.supports_operation(verbs::LIST).unwrap() { + /// if !ar.supports_operation(verbs::LIST) { /// continue; /// } /// let api: Api = Api::all_with(client.clone(), &ar); diff --git a/kube-client/src/discovery/parse.rs b/kube-client/src/discovery/parse.rs index e5372ed16..934f2a0af 100644 --- a/kube-client/src/discovery/parse.rs +++ b/kube-client/src/discovery/parse.rs @@ -13,6 +13,7 @@ pub(crate) fn parse_apiresource( ) -> 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 @@ -24,8 +25,7 @@ pub(crate) fn parse_apiresource( api_version: gv.api_version(), kind: ar.kind.to_string(), plural: ar.name.clone(), - namespaced: ar.namespaced, - capabilities: Some(caps), + capabilities: caps, }) } @@ -68,9 +68,7 @@ impl GroupVersionData { parse_apiresource(res, &list.group_version).map_err(|ParseGroupVersionError(s)| { Error::Discovery(DiscoveryError::InvalidGroupVersion(s)) })?; - if let Some(caps) = &mut ar.capabilities { - caps.subresources = find_subresources(&list, &res.name)?; - } + ar.capabilities.subresources = find_subresources(&list, &res.name)?; resources.push(ar); } Ok(GroupVersionData { version, resources }) diff --git a/kube-core/src/discovery.rs b/kube-core/src/discovery.rs index aa7f949a4..7d9390647 100644 --- a/kube-core/src/discovery.rs +++ b/kube-core/src/discovery.rs @@ -37,28 +37,31 @@ pub struct ApiResource { pub kind: String, /// Resource name / plural name pub plural: String, - /// Whether the resource is namespaced - pub namespaced: bool, - /// Capabilities from kube-derive or api discovery - pub capabilities: Option, + /// 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` does not + /// 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(), - namespaced: K::is_namespaced(dt), - capabilities: None, + capabilities: caps, } } @@ -76,8 +79,7 @@ impl ApiResource { version: gvk.version.clone(), kind: gvk.kind.clone(), plural: plural.to_string(), - namespaced: false, - capabilities: None, + capabilities: ApiCapabilities::default(), } } @@ -99,15 +101,32 @@ impl ApiResource { ApiResource::new(gvk, &to_plural(&gvk.kind.to_ascii_lowercase())) } - /// Attach capabilities to a manually constructed [`ApiResource`] - pub fn with_caps(mut self, caps: ApiCapabilities) -> Self { - self.capabilities = Some(caps); - self + /// Get the namespaced property + pub fn namespaced(&self) -> bool { + self.capabilities.namespaced } /// Set the whether the resource is namsepace scoped - pub fn namespaced(mut self, namespaced: bool) -> Self { - self.namespaced = namespaced; + 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 } } @@ -117,6 +136,8 @@ impl 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 @@ -152,39 +173,10 @@ pub mod verbs { impl ApiResource { /// Checks that given verb is supported on this resource. /// - /// If verbs are missing, we return a None to indicate - /// that we do not have enough information to say true or false. - pub fn supports_operation(&self, operation: &str) -> Option { - if let Some(caps) = &self.capabilities { - Some(caps.verbs.iter().any(|op| op == operation)) - } else { - None - } - } -} - -impl ApiCapabilities { - /// Set the shortnames - pub fn shortnames(mut self, shortnames: &[&str]) -> Self { - self.shortnames = shortnames.iter().map(|x| x.to_string()).collect(); - self - } - - /// Set the allowed verbs - pub fn verbs(mut self, verbs: &[&str]) -> Self { - self.verbs = verbs.iter().map(|x| x.to_string()).collect(); - self - } - - /// Set the default verbs - pub fn default_verbs(mut self) -> Self { - self.verbs = verbs::DEFAULT_VERBS.iter().map(|x| x.to_string()).collect(); - self - } - - /// 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.verbs.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 6c94b97b2..6d31ad7f0 100644 --- a/kube-core/src/dynamic.rs +++ b/kube-core/src/dynamic.rs @@ -89,7 +89,7 @@ impl Resource for DynamicObject { } fn is_namespaced(dt: &ApiResource) -> bool { - dt.namespaced + dt.capabilities.namespaced } } diff --git a/kube-core/src/object.rs b/kube-core/src/object.rs index 1476968c4..25429035e 100644 --- a/kube-core/src/object.rs +++ b/kube-core/src/object.rs @@ -248,7 +248,7 @@ where } fn is_namespaced(dt: &ApiResource) -> bool { - dt.namespaced + dt.capabilities.namespaced } } diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index 43177a6d5..71eb85658 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -457,10 +457,9 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea } fn api_resource() -> #kube_core::ApiResource { - let caps = #kube_core::ApiCapabilities::default().shortnames(#shortnames_slice).default_verbs(); // TODO: populate subresources #kube_core::ApiResource::erase::(&()) - .with_caps(caps) + .set_shortnames(#shortnames_slice).set_default_verbs() } diff --git a/kube/src/lib.rs b/kube/src/lib.rs index a801278c9..3e2c1828a 100644 --- a/kube/src/lib.rs +++ b/kube/src/lib.rs @@ -414,11 +414,10 @@ mod test { assert_eq!(firstgroup.name(), ApiGroup::CORE_GROUP, "core not first"); for group in groups { for ar in group.recommended_resources() { - let caps = ar.capabilities.as_ref().unwrap(); - if !caps.supports_operation(verbs::LIST) { + if !ar.supports_operation(verbs::LIST) { continue; } - let api: Api = if ar.namespaced { + let api: Api = if ar.namespaced() { Api::default_namespaced_with(client.clone(), &ar) } else { Api::all_with(client.clone(), &ar)