From 78d06d227efbc0887ad0ed2516248bb813cc772a Mon Sep 17 00:00:00 2001 From: Mikail Bagishov Date: Sat, 17 Apr 2021 01:23:25 +0300 Subject: [PATCH 1/8] Separate GVK from ApiResource --- examples/crd_derive_schema.rs | 11 +- examples/dynamic_api.rs | 5 +- examples/dynamic_watcher.rs | 5 +- kube-runtime/src/reflector/object_ref.rs | 15 +- kube/src/api/dynamic.rs | 302 ++++++++++++----------- kube/src/api/gvk.rs | 83 +++++++ kube/src/api/mod.rs | 5 +- 7 files changed, 262 insertions(+), 164 deletions(-) create mode 100644 kube/src/api/gvk.rs diff --git a/examples/crd_derive_schema.rs b/examples/crd_derive_schema.rs index 73bdac404..9569df873 100644 --- a/examples/crd_derive_schema.rs +++ b/examples/crd_derive_schema.rs @@ -3,8 +3,8 @@ use futures::{StreamExt, TryStreamExt}; use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition; use kube::{ api::{ - Api, DeleteParams, DynamicObject, GroupVersionKind, ListParams, Patch, PatchParams, PostParams, - WatchEvent, + Api, ApiResource, DeleteParams, DynamicObject, GroupVersionKind, ListParams, Patch, PatchParams, + PostParams, WatchEvent, }, Client, CustomResource, }; @@ -149,7 +149,8 @@ async fn main() -> Result<()> { // Set up dynamic resource to test using raw values. let gvk = GroupVersionKind::gvk("clux.dev", "v1", "Foo")?; - let dynapi: Api = Api::namespaced_with(client.clone(), &namespace, &gvk); + let api_resource = ApiResource::from_gvk(&gvk); + let dynapi: Api = Api::namespaced_with(client.clone(), &namespace, &api_resource); // Test that skipped nullable field without default is not defined. let val = dynapi.get("bar").await?.data; @@ -160,7 +161,7 @@ async fn main() -> Result<()> { assert!(spec.contains_key("nullable")); // Test defaulting of `non_nullable_with_default` field - let data = DynamicObject::new("baz", &gvk).data(serde_json::json!({ + let data = DynamicObject::new("baz", &api_resource).data(serde_json::json!({ "spec": { "non_nullable": "a required field", // `non_nullable_with_default` field is missing @@ -180,7 +181,7 @@ async fn main() -> Result<()> { assert_eq!(serde_json::to_string(&val["spec"]["set_listable"])?, "[2]"); // Missing required field (non-nullable without default) is an error - let data = DynamicObject::new("qux", &gvk).data(serde_json::json!({ + let data = DynamicObject::new("qux", &api_resource).data(serde_json::json!({ "spec": {} })); let res = dynapi.create(&PostParams::default(), &data).await; diff --git a/examples/dynamic_api.rs b/examples/dynamic_api.rs index c4ab1d42c..05954483a 100644 --- a/examples/dynamic_api.rs +++ b/examples/dynamic_api.rs @@ -2,9 +2,8 @@ //! to `kubectl get all --all-namespaces`. use kube::{ - api::{Api, DynamicObject, Resource, ResourceExt}, - client::Discovery, - Client, + api::{Api, ApiResource, DynamicObject, Resource, ResourceExt}, + client::{Client, Discovery}, }; use log::{info, warn}; diff --git a/examples/dynamic_watcher.rs b/examples/dynamic_watcher.rs index 947cc778a..6ba4d3545 100644 --- a/examples/dynamic_watcher.rs +++ b/examples/dynamic_watcher.rs @@ -1,6 +1,6 @@ use futures::prelude::*; use kube::{ - api::{DynamicObject, GroupVersionKind, ListParams, ResourceExt}, + api::{ApiResource, DynamicObject, GroupVersionKind, ListParams, ResourceExt}, Api, Client, }; use kube_runtime::{utils::try_flatten_applied, watcher}; @@ -19,8 +19,9 @@ async fn main() -> anyhow::Result<()> { // Turn them into a GVK let gvk = GroupVersionKind::gvk(&group, &version, &kind)?; + let api_resource = ApiResource::from_gvk(&gvk); // Use them in an Api with the GVK as its DynamicType - let api = Api::::all_with(client, &gvk); + let api = Api::::all_with(client, &api_resource); // Fully compatible with kube-runtime let watcher = watcher(api, ListParams::default()); diff --git a/kube-runtime/src/reflector/object_ref.rs b/kube-runtime/src/reflector/object_ref.rs index 3dea80d74..f5aa4f7d5 100644 --- a/kube-runtime/src/reflector/object_ref.rs +++ b/kube-runtime/src/reflector/object_ref.rs @@ -129,13 +129,16 @@ impl ObjectRef { } pub fn erase(self) -> ObjectRef { + let gvk = kube::api::GroupVersionKind::gvk( + K::group(&self.dyntype).as_ref(), + K::version(&self.dyntype).as_ref(), + K::kind(&self.dyntype).as_ref(), + ) + .expect("valid gvk"); + let mut res = kube::api::ApiResource::from_gvk(&gvk); + res.plural_name = K::plural(&self.dyntype).to_string(); ObjectRef { - dyntype: kube::api::GroupVersionKind::gvk( - K::group(&self.dyntype).as_ref(), - K::version(&self.dyntype).as_ref(), - K::kind(&self.dyntype).as_ref(), - ) - .expect("valid gvk"), + dyntype: res, name: self.name, namespace: self.namespace, } diff --git a/kube/src/api/dynamic.rs b/kube/src/api/dynamic.rs index 2b480a607..dd1baa04a 100644 --- a/kube/src/api/dynamic.rs +++ b/kube/src/api/dynamic.rs @@ -1,32 +1,87 @@ -use crate::{ - api::{metadata::TypeMeta, Resource}, - Error, Result, -}; +use crate::api::{metadata::TypeMeta, GroupVersionKind, Resource}; use k8s_openapi::apimachinery::pkg::apis::meta::v1::{APIResource, ObjectMeta}; -use serde::{Deserialize, Serialize}; use std::borrow::Cow; -/// Represents a type-erased object kind -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)] -pub struct GroupVersionKind { - /// API group - group: String, - /// Version - version: String, - /// Kind - pub(crate) kind: String, - /// Concatenation of group and version - #[serde(default)] - api_version: String, - /// Optional plural/resource - plural: Option, +/// Resource scope +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub enum Scope { + /// Objects are global + Cluster, + /// Each object lives in namespace. + Namespaced, } -impl GroupVersionKind { - /// Creates `GroupVersionKind` from an [`APIResource`]. +/// Operations that are supported on the resource +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct Operations { + /// Object can be created + pub create: bool, + /// Single object can be queried + pub get: bool, + /// Multiple objects can be queried + pub list: bool, + /// A watch can be started + pub watch: bool, + /// A single object can be deleted + pub delete: bool, + /// Multiple objects can be deleted + pub delete_collection: bool, + /// Object can be updated + pub update: bool, + /// Object can be patched + pub patch: bool, + /// All other verbs + pub other: Vec, +} + +impl Operations { + /// Returns empty `Operations` + pub fn empty() -> Self { + Operations { + create: false, + get: false, + list: false, + watch: false, + delete: false, + delete_collection: false, + update: false, + patch: false, + other: Vec::new(), + } + } +} + +/// Contains information about Kubernetes API resources +/// which is either required or helpful for working with it. +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct ApiResource { + /// Resource group, empty for core group. + pub group: String, + /// group version + pub version: String, + /// apiVersion of the resource (v1 for core group, + /// groupName/groupVersions for other). + pub api_version: String, + /// Singular PascalCase name of the resource + pub kind: String, + /// Plural name of the resource + pub plural_name: String, + /// Scope of the resource + pub scope: Scope, + /// Names of the available subresources + pub subresources: Vec, + /// Supported operations on this resource + pub operations: Operations, +} + +impl ApiResource { + /// Creates ApiResource from `meta::v1::APIResource` instance. + /// This is recommended way to create `ApiResource`s. + /// This function correctly sets all fields except `subresources`. /// /// `APIResource` objects can be extracted from [`Client::list_api_group_resources`](crate::Client::list_api_group_resources). - /// If it does not specify version and/or group, they will be taken from `group_version`. + /// If it does not specify version and/or group, they will be taken from `group_version` + /// (otherwise the second parameter is ignored). /// /// ### Example usage: /// ``` @@ -41,7 +96,7 @@ impl GroupVersionKind { /// # Ok(()) /// # } /// ``` - pub fn from_api_resource(ar: &APIResource, group_version: &str) -> Self { + pub fn from_apiresource(ar: &APIResource, group_version: &str) -> Self { let gvsplit = group_version.splitn(2, '/').collect::>(); let (default_group, default_version) = match *gvsplit.as_slice() { [g, v] => (g, v), // standard case @@ -56,125 +111,78 @@ impl GroupVersionKind { } else { format!("{}/{}", group, version) }; - let plural = Some(ar.name.clone()); - Self { - group, - version, - kind, - api_version, - plural, - } - } - - /// Set the api group, version, and kind for a resource - pub fn gvk(group_: &str, version_: &str, kind_: &str) -> Result { - let version = version_.to_string(); - let group = group_.to_string(); - let kind = kind_.to_string(); - let api_version = if group.is_empty() { - version.to_string() + let plural_name = ar.name.clone(); + let scope = if ar.namespaced { + Scope::Namespaced } else { - format!("{}/{}", group, version) + Scope::Cluster }; - if version.is_empty() { - return Err(Error::DynamicType(format!( - "GroupVersionKind '{}' must have a version", - kind - ))); + let mut operations = Operations::empty(); + for verb in &ar.verbs { + match verb.as_str() { + "create" => operations.create = true, + "get" => operations.get = true, + "list" => operations.list = true, + "watch" => operations.watch = true, + "delete" => operations.delete = true, + "deletecollection" => operations.delete_collection = true, + "update" => operations.update = true, + "patch" => operations.patch = true, + _ => operations.other.push(verb.clone()), + } } - if kind.is_empty() { - return Err(Error::DynamicType(format!( - "GroupVersionKind '{}' must have a kind", - kind - ))); - } - Ok(Self { + ApiResource { group, version, kind, api_version, - plural: None, - }) - } - - /// Set an explicit plural/resource value to avoid relying on inferred pluralisation. - pub fn plural(mut self, plural: &str) -> Self { - self.plural = Some(plural.to_string()); - self - } -} - -/// Represents a type-erased object resource. -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)] -pub struct GroupVersionResource { - /// API group - group: String, - /// Version - version: String, - /// Resource - resource: String, - /// Concatenation of group and version - #[serde(default)] - api_version: String, -} - -impl GroupVersionResource { - /// Creates `GroupVersionResource` from an [`APIResource`]. - pub fn from_api_resource(ar: &APIResource, group_version: &str) -> Self { - let gvsplit = group_version.splitn(2, '/').collect::>(); - let (default_group, default_version) = match *gvsplit.as_slice() { - [g, v] => (g, v), // standard case - [v] => ("", v), // core v1 case - _ => unreachable!(), - }; - let group = ar.group.clone().unwrap_or_else(|| default_group.into()); - let version = ar.version.clone().unwrap_or_else(|| default_version.into()); - let resource = ar.name.to_string(); - let api_version = if group.is_empty() { - version.clone() - } else { - format!("{}/{}", group, version) - }; - Self { - group, - version, - resource, - api_version, + plural_name, + scope, + subresources: Vec::new(), + operations, } } - /// Set the api group, version, and the plural resource name. - pub fn gvr(group_: &str, version_: &str, resource_: &str) -> Result { - let version = version_.to_string(); - let group = group_.to_string(); - let resource = resource_.to_string(); - let api_version = if group.is_empty() { - version.to_string() - } else { - format!("{}/{}", group, version) + /// Creates ApiResource from group, version and kind. + /// # Warning + /// This function has to **guess** some information. + /// While it makes it best to guess correctly, sometimes it can + /// be wrong, and using returned ApiResource will lead to incorrect + /// api requests. + /// In more details: + /// - `scope` is assumed to be `Namespaced` + /// - `plural_name` is derived from `kind` (can lead to incorrect request) + /// - `operations` and `subresources` are filled with values which + /// are expected for most resources + pub fn from_gvk(gvk: &GroupVersionKind) -> Self { + let api_version = match gvk.group.as_str() { + "" => gvk.version.clone(), + _ => format!("{}/{}", gvk.group, gvk.version), }; - if version.is_empty() { - return Err(Error::DynamicType(format!( - "GroupVersionResource '{}' must have a version", - resource - ))); - } - if resource.is_empty() { - return Err(Error::DynamicType(format!( - "GroupVersionResource '{}' must have a resource", - resource - ))); - } - Ok(Self { - group, - version, - resource, + ApiResource { + group: gvk.group.clone(), + version: gvk.version.clone(), api_version, - }) + kind: gvk.kind.clone(), + plural_name: crate::api::metadata::to_plural(&gvk.kind.to_ascii_lowercase()), + scope: Scope::Namespaced, + subresources: vec!["status".to_string()], + operations: Operations { + create: true, + get: true, + list: true, + watch: true, + delete: true, + delete_collection: true, + update: true, + patch: true, + other: Vec::new(), + }, + } } } -/// A dynamic representation of a kubernetes resource +/// A dynamic representation of a kubernetes object /// /// This will work with any non-list type object. #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] @@ -191,12 +199,12 @@ pub struct DynamicObject { } impl DynamicObject { - /// Create a DynamicObject with minimal values set from GVK. - pub fn new(name: &str, gvk: &GroupVersionKind) -> Self { + /// Create a DynamicObject with minimal values set from ApiResource. + pub fn new(name: &str, resource: &ApiResource) -> Self { Self { types: Some(TypeMeta { - api_version: gvk.api_version.to_string(), - kind: gvk.kind.to_string(), + api_version: resource.api_version.to_string(), + kind: resource.kind.to_string(), }), metadata: ObjectMeta { name: Some(name.to_string()), @@ -220,31 +228,26 @@ impl DynamicObject { } impl Resource for DynamicObject { - type DynamicType = GroupVersionKind; + type DynamicType = ApiResource; - fn group(dt: &GroupVersionKind) -> Cow<'_, str> { + fn group(dt: &ApiResource) -> Cow<'_, str> { dt.group.as_str().into() } - fn version(dt: &GroupVersionKind) -> Cow<'_, str> { + fn version(dt: &ApiResource) -> Cow<'_, str> { dt.version.as_str().into() } - fn kind(dt: &GroupVersionKind) -> Cow<'_, str> { + fn kind(dt: &ApiResource) -> Cow<'_, str> { dt.kind.as_str().into() } - fn api_version(dt: &GroupVersionKind) -> Cow<'_, str> { + fn api_version(dt: &ApiResource) -> Cow<'_, str> { dt.api_version.as_str().into() } - fn plural(dt: &Self::DynamicType) -> Cow<'_, str> { - if let Some(plural) = &dt.plural { - plural.into() - } else { - // fallback to inference - crate::api::metadata::to_plural(&Self::kind(dt).to_ascii_lowercase()).into() - } + fn plural(dt: &ApiResource) -> Cow<'_, str> { + dt.plural_name.as_str().into() } fn meta(&self) -> &ObjectMeta { @@ -259,13 +262,16 @@ impl Resource for DynamicObject { #[cfg(test)] mod test { use crate::{ - api::{DynamicObject, GroupVersionKind, Patch, PatchParams, PostParams, Request, Resource}, + api::{ + ApiResource, DynamicObject, GroupVersionKind, Patch, PatchParams, PostParams, Request, Resource, + }, Result, }; #[test] fn raw_custom_resource() { let gvk = GroupVersionKind::gvk("clux.dev", "v1", "Foo").unwrap(); - let url = DynamicObject::url_path(&gvk, Some("myns")); + let res = ApiResource::from_gvk(&gvk); + let url = DynamicObject::url_path(&res, Some("myns")); let pp = PostParams::default(); let req = Request::new(&url).create(&pp, vec![]).unwrap(); @@ -281,7 +287,8 @@ mod test { #[test] fn raw_resource_in_default_group() -> Result<()> { let gvk = GroupVersionKind::gvk("", "v1", "Service").unwrap(); - let url = DynamicObject::url_path(&gvk, None); + let api_resource = ApiResource::from_gvk(&gvk); + let url = DynamicObject::url_path(&api_resource, None); let pp = PostParams::default(); let req = Request::new(url).create(&pp, vec![])?; assert_eq!(req.uri(), "/api/v1/services?"); @@ -304,7 +311,8 @@ mod test { let client = Client::try_default().await.unwrap(); let gvk = GroupVersionKind::gvk("clux.dev", "v1", "Foo").unwrap(); - let a1: Api = Api::namespaced_with(client.clone(), "myns", &gvk); + let api_resource = ApiResource::from_gvk(&gvk); + let a1: Api = Api::namespaced_with(client.clone(), "myns", &api_resource); let a2: Api = Api::namespaced(client.clone(), "myns"); // make sure they return the same url_path through their impls diff --git a/kube/src/api/gvk.rs b/kube/src/api/gvk.rs new file mode 100644 index 000000000..587b7c772 --- /dev/null +++ b/kube/src/api/gvk.rs @@ -0,0 +1,83 @@ +use crate::{Error, Result}; +use serde::{Deserialize, Serialize}; + + +/// Contains enough information to identify API Resource. +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)] +pub struct GroupVersionKind { + /// API group + pub group: String, + /// Version + pub version: String, + /// Kind + pub kind: String, +} + +impl GroupVersionKind { + /// Set the api group, version, and kind for a resource + pub fn gvk(group_: &str, version_: &str, kind_: &str) -> Result { + let version = version_.to_string(); + let group = group_.to_string(); + let kind = kind_.to_string(); + if version.is_empty() { + return Err(Error::DynamicType(format!( + "GroupVersionKind '{}' must have a version", + kind + ))); + } + if kind.is_empty() { + return Err(Error::DynamicType(format!( + "GroupVersionKind '{}' must have a kind", + kind + ))); + } + Ok(Self { group, version, kind }) + } +} + + +/// Represents a type-erased object resource. +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)] +pub struct GroupVersionResource { + /// API group + pub group: String, + /// Version + pub version: String, + /// Resource + pub resource: String, + /// Concatenation of group and version + #[serde(default)] + api_version: String, +} + +impl GroupVersionResource { + /// Set the api group, version, and the plural resource name. + pub fn gvr(group_: &str, version_: &str, resource_: &str) -> Result { + let version = version_.to_string(); + let group = group_.to_string(); + let resource = resource_.to_string(); + let api_version = if group.is_empty() { + version.to_string() + } else { + format!("{}/{}", group, version) + }; + if version.is_empty() { + return Err(Error::DynamicType(format!( + "GroupVersionResource '{}' must have a version", + resource + ))); + } + if resource.is_empty() { + return Err(Error::DynamicType(format!( + "GroupVersionResource '{}' must have a resource", + resource + ))); + } + Ok(Self { + group, + version, + resource, + api_version, + }) + } +} diff --git a/kube/src/api/mod.rs b/kube/src/api/mod.rs index 63b6c5051..2b1b7a8fb 100644 --- a/kube/src/api/mod.rs +++ b/kube/src/api/mod.rs @@ -19,8 +19,11 @@ pub use request::Request; pub(crate) mod typed; pub use typed::Api; +mod gvk; +pub use gvk::{GroupVersionKind, GroupVersionResource}; + mod dynamic; -pub use dynamic::{DynamicObject, GroupVersionKind, GroupVersionResource}; +pub use dynamic::{ApiResource, DynamicObject}; #[cfg(feature = "ws")] mod remote_command; #[cfg(feature = "ws")] pub use remote_command::AttachedProcess; From 7e3cc6ff7820fb907e174682f8bdbd017fa69437 Mon Sep 17 00:00:00 2001 From: Mikail Bagishov Date: Sat, 17 Apr 2021 15:12:36 +0300 Subject: [PATCH 2/8] Move details to other type --- kube/src/api/dynamic.rs | 211 +++++++++++++++++++++------------------- kube/src/api/mod.rs | 2 +- 2 files changed, 114 insertions(+), 99 deletions(-) diff --git a/kube/src/api/dynamic.rs b/kube/src/api/dynamic.rs index dd1baa04a..560198d09 100644 --- a/kube/src/api/dynamic.rs +++ b/kube/src/api/dynamic.rs @@ -1,58 +1,9 @@ use crate::api::{metadata::TypeMeta, GroupVersionKind, Resource}; -use k8s_openapi::apimachinery::pkg::apis::meta::v1::{APIResource, ObjectMeta}; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::{APIResource, APIResourceList, ObjectMeta}; use std::borrow::Cow; -/// Resource scope -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub enum Scope { - /// Objects are global - Cluster, - /// Each object lives in namespace. - Namespaced, -} - -/// Operations that are supported on the resource -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct Operations { - /// Object can be created - pub create: bool, - /// Single object can be queried - pub get: bool, - /// Multiple objects can be queried - pub list: bool, - /// A watch can be started - pub watch: bool, - /// A single object can be deleted - pub delete: bool, - /// Multiple objects can be deleted - pub delete_collection: bool, - /// Object can be updated - pub update: bool, - /// Object can be patched - pub patch: bool, - /// All other verbs - pub other: Vec, -} - -impl Operations { - /// Returns empty `Operations` - pub fn empty() -> Self { - Operations { - create: false, - get: false, - list: false, - watch: false, - delete: false, - delete_collection: false, - update: false, - patch: false, - other: Vec::new(), - } - } -} - /// Contains information about Kubernetes API resources -/// which is either required or helpful for working with it. +/// which is enough for working with it. #[derive(Debug, Clone, Hash, Eq, PartialEq)] pub struct ApiResource { /// Resource group, empty for core group. @@ -66,12 +17,6 @@ pub struct ApiResource { pub kind: String, /// Plural name of the resource pub plural_name: String, - /// Scope of the resource - pub scope: Scope, - /// Names of the available subresources - pub subresources: Vec, - /// Supported operations on this resource - pub operations: Operations, } impl ApiResource { @@ -112,48 +57,21 @@ impl ApiResource { format!("{}/{}", group, version) }; let plural_name = ar.name.clone(); - let scope = if ar.namespaced { - Scope::Namespaced - } else { - Scope::Cluster - }; - let mut operations = Operations::empty(); - for verb in &ar.verbs { - match verb.as_str() { - "create" => operations.create = true, - "get" => operations.get = true, - "list" => operations.list = true, - "watch" => operations.watch = true, - "delete" => operations.delete = true, - "deletecollection" => operations.delete_collection = true, - "update" => operations.update = true, - "patch" => operations.patch = true, - _ => operations.other.push(verb.clone()), - } - } ApiResource { group, version, kind, api_version, plural_name, - scope, - subresources: Vec::new(), - operations, } } /// Creates ApiResource from group, version and kind. /// # Warning - /// This function has to **guess** some information. + /// This function has to **guess** resource plural name. /// While it makes it best to guess correctly, sometimes it can /// be wrong, and using returned ApiResource will lead to incorrect /// api requests. - /// In more details: - /// - `scope` is assumed to be `Namespaced` - /// - `plural_name` is derived from `kind` (can lead to incorrect request) - /// - `operations` and `subresources` are filled with values which - /// are expected for most resources pub fn from_gvk(gvk: &GroupVersionKind) -> Self { let api_version = match gvk.group.as_str() { "" => gvk.version.clone(), @@ -165,19 +83,6 @@ impl ApiResource { api_version, kind: gvk.kind.clone(), plural_name: crate::api::metadata::to_plural(&gvk.kind.to_ascii_lowercase()), - scope: Scope::Namespaced, - subresources: vec!["status".to_string()], - operations: Operations { - create: true, - get: true, - list: true, - watch: true, - delete: true, - delete_collection: true, - update: true, - patch: true, - other: Vec::new(), - }, } } } @@ -259,6 +164,116 @@ impl Resource for DynamicObject { } } +/// Resource scope +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub enum Scope { + /// Objects are global + Cluster, + /// Each object lives in namespace. + Namespaced, +} + +/// Operations that are supported on the resource +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct Operations { + /// Object can be created + pub create: bool, + /// Single object can be queried + pub get: bool, + /// Multiple objects can be queried + pub list: bool, + /// A watch can be started + pub watch: bool, + /// A single object can be deleted + pub delete: bool, + /// Multiple objects can be deleted + pub delete_collection: bool, + /// Object can be updated + pub update: bool, + /// Object can be patched + pub patch: bool, + /// All other verbs + pub other: Vec, +} + +impl Operations { + /// Returns empty `Operations` + pub fn empty() -> Self { + Operations { + create: false, + get: false, + list: false, + watch: false, + delete: false, + delete_collection: false, + update: false, + patch: false, + other: Vec::new(), + } + } +} +/// Contains additional, detailed information abount API resource +pub struct ApiResourceExtras { + /// 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. + pub subresources: Vec<(ApiResource, ApiResourceExtras)>, + /// Supported operations on this resource + pub operations: Operations, +} + +impl ApiResourceExtras { + /// Creates ApiResourceExtras from `meta::v1::APIResourceList` instance. + /// This function correctly sets all fields except `subresources`. + /// # Panics + /// Panics if list does not contain resource named `name`. + pub fn from_apiresourcelist(list: &APIResourceList, name: &str) -> Self { + let ar = list + .resources + .iter() + .find(|r| r.name == name) + .expect("resource not found in APIResourceList"); + let scope = if ar.namespaced { + Scope::Namespaced + } else { + Scope::Cluster + }; + let mut operations = Operations::empty(); + for verb in &ar.verbs { + match verb.as_str() { + "create" => operations.create = true, + "get" => operations.get = true, + "list" => operations.list = true, + "watch" => operations.watch = true, + "delete" => operations.delete = true, + "deletecollection" => operations.delete_collection = true, + "update" => operations.update = true, + "patch" => operations.patch = true, + _ => operations.other.push(verb.clone()), + } + } + let mut subresources = Vec::new(); + let subresource_name_prefix = format!("{}/", name); + for res in &list.resources { + if let Some(subresource_name) = res.name.strip_prefix(&subresource_name_prefix) { + let mut api_resource = ApiResource::from_apiresource(res, &list.group_version); + api_resource.plural_name = subresource_name.to_string(); + let extra = ApiResourceExtras::from_apiresourcelist(list, &res.name); + subresources.push((api_resource, extra)); + } + } + + ApiResourceExtras { + scope, + subresources, + operations, + } + } +} + #[cfg(test)] mod test { use crate::{ diff --git a/kube/src/api/mod.rs b/kube/src/api/mod.rs index 2b1b7a8fb..f6507e526 100644 --- a/kube/src/api/mod.rs +++ b/kube/src/api/mod.rs @@ -23,7 +23,7 @@ mod gvk; pub use gvk::{GroupVersionKind, GroupVersionResource}; mod dynamic; -pub use dynamic::{ApiResource, DynamicObject}; +pub use dynamic::{ApiResource, ApiResourceExtras, DynamicObject, Operations, Scope}; #[cfg(feature = "ws")] mod remote_command; #[cfg(feature = "ws")] pub use remote_command::AttachedProcess; From 81069879d12c835edb4f3123a6bde0416b06e626 Mon Sep 17 00:00:00 2001 From: Mikail Bagishov Date: Sun, 18 Apr 2021 14:43:59 +0300 Subject: [PATCH 3/8] Partially implement review suggestions --- kube-derive/src/custom_resource.rs | 6 +++++- kube-runtime/src/reflector/object_ref.rs | 4 ++-- kube/src/api/dynamic.rs | 23 +++++++++++++++++------ 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index 62dbe0e50..dd534da67 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -329,7 +329,7 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea } }; - // Implement the ::crd method (fine to not have in a trait as its a generated type) + // Implement the ::crd and ::api_resource methods (fine to not have in a trait as its a generated type) let impl_crd = quote! { impl #rootident { pub fn crd() -> #apiext::CustomResourceDefinition { @@ -357,6 +357,10 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea serde_json::from_value(jsondata) .expect("valid custom resource from #[kube(attrs..)]") } + + pub fn api_resource() -> kube::api::ApiResource { + kube::api::ApiResource::erase::(&()) + } } }; diff --git a/kube-runtime/src/reflector/object_ref.rs b/kube-runtime/src/reflector/object_ref.rs index f5aa4f7d5..86fe6f61a 100644 --- a/kube-runtime/src/reflector/object_ref.rs +++ b/kube-runtime/src/reflector/object_ref.rs @@ -136,9 +136,9 @@ impl ObjectRef { ) .expect("valid gvk"); let mut res = kube::api::ApiResource::from_gvk(&gvk); - res.plural_name = K::plural(&self.dyntype).to_string(); + res.plural = K::plural(&self.dyntype).to_string(); ObjectRef { - dyntype: res, + dyntype: kube::api::ApiResource::erase::(&self.dyntype), name: self.name, namespace: self.namespace, } diff --git a/kube/src/api/dynamic.rs b/kube/src/api/dynamic.rs index 560198d09..a99184a17 100644 --- a/kube/src/api/dynamic.rs +++ b/kube/src/api/dynamic.rs @@ -16,7 +16,7 @@ pub struct ApiResource { /// Singular PascalCase name of the resource pub kind: String, /// Plural name of the resource - pub plural_name: String, + pub plural: String, } impl ApiResource { @@ -56,13 +56,24 @@ impl ApiResource { } else { format!("{}/{}", group, version) }; - let plural_name = ar.name.clone(); + let plural = ar.name.clone(); ApiResource { group, version, kind, api_version, - plural_name, + plural, + } + } + + /// Creates ApiResource by type-erasing another Resource + pub fn erase(dt: &K::DynamicType) -> Self { + 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(), } } @@ -82,7 +93,7 @@ impl ApiResource { version: gvk.version.clone(), api_version, kind: gvk.kind.clone(), - plural_name: crate::api::metadata::to_plural(&gvk.kind.to_ascii_lowercase()), + plural: crate::api::metadata::to_plural(&gvk.kind.to_ascii_lowercase()), } } } @@ -152,7 +163,7 @@ impl Resource for DynamicObject { } fn plural(dt: &ApiResource) -> Cow<'_, str> { - dt.plural_name.as_str().into() + dt.plural.as_str().into() } fn meta(&self) -> &ObjectMeta { @@ -260,7 +271,7 @@ impl ApiResourceExtras { for res in &list.resources { if let Some(subresource_name) = res.name.strip_prefix(&subresource_name_prefix) { let mut api_resource = ApiResource::from_apiresource(res, &list.group_version); - api_resource.plural_name = subresource_name.to_string(); + api_resource.plural = subresource_name.to_string(); let extra = ApiResourceExtras::from_apiresourcelist(list, &res.name); subresources.push((api_resource, extra)); } From 369278f888a1ff8e0279b7feed6438ee50a06a04 Mon Sep 17 00:00:00 2001 From: Mikail Bagishov Date: Mon, 19 Apr 2021 14:54:50 +0300 Subject: [PATCH 4/8] update --- kube/src/api/dynamic.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/kube/src/api/dynamic.rs b/kube/src/api/dynamic.rs index a99184a17..e6a92d363 100644 --- a/kube/src/api/dynamic.rs +++ b/kube/src/api/dynamic.rs @@ -21,8 +21,6 @@ pub struct ApiResource { impl ApiResource { /// Creates ApiResource from `meta::v1::APIResource` instance. - /// This is recommended way to create `ApiResource`s. - /// This function correctly sets all fields except `subresources`. /// /// `APIResource` objects can be extracted from [`Client::list_api_group_resources`](crate::Client::list_api_group_resources). /// If it does not specify version and/or group, they will be taken from `group_version` @@ -30,13 +28,13 @@ impl ApiResource { /// /// ### Example usage: /// ``` - /// use kube::api::{GroupVersionKind, Api, DynamicObject}; + /// use kube::api::{ApiResource, Api, DynamicObject}; /// # async fn scope(client: kube::Client) -> Result<(), Box> { /// let apps = client.list_api_group_resources("apps/v1").await?; /// for ar in &apps.resources { - /// let gvk = GroupVersionKind::from_api_resource(ar, &apps.group_version); - /// dbg!(&gvk); - /// let api: Api = Api::namespaced_with(client.clone(), "default", &gvk); + /// let resource = ApiResource::from_apiresource(ar, &apps.group_version); + /// dbg!(&resource); + /// let api: Api = Api::namespaced_with(client.clone(), "default", &resource); /// } /// # Ok(()) /// # } @@ -84,6 +82,14 @@ impl ApiResource { /// be wrong, and using returned ApiResource will lead to incorrect /// api requests. pub fn from_gvk(gvk: &GroupVersionKind) -> Self { + ApiResource::from_gvk_with_plural( + gvk, + &crate::api::metadata::to_plural(&gvk.kind.to_ascii_lowercase()), + ) + } + + /// Creates ApiResource from group, version, kind and plural name. + pub fn from_gvk_with_plural(gvk: &GroupVersionKind, plural: &str) -> Self { let api_version = match gvk.group.as_str() { "" => gvk.version.clone(), _ => format!("{}/{}", gvk.group, gvk.version), @@ -93,7 +99,7 @@ impl ApiResource { version: gvk.version.clone(), api_version, kind: gvk.kind.clone(), - plural: crate::api::metadata::to_plural(&gvk.kind.to_ascii_lowercase()), + plural: plural.to_string(), } } } From 267b7b164a7321f14b19f3e4f3003aa134b5484e Mon Sep 17 00:00:00 2001 From: Mikail Bagishov Date: Mon, 19 Apr 2021 14:58:31 +0300 Subject: [PATCH 5/8] Make GVK and GVR constructors infallible --- examples/crd_derive_schema.rs | 2 +- examples/dynamic_watcher.rs | 2 +- kube-runtime/src/reflector/object_ref.rs | 8 ----- kube/src/api/dynamic.rs | 6 ++-- kube/src/api/gvk.rs | 39 +++++------------------- kube/src/error.rs | 4 --- 6 files changed, 12 insertions(+), 49 deletions(-) diff --git a/examples/crd_derive_schema.rs b/examples/crd_derive_schema.rs index 9569df873..0cac1b6c9 100644 --- a/examples/crd_derive_schema.rs +++ b/examples/crd_derive_schema.rs @@ -148,7 +148,7 @@ async fn main() -> Result<()> { }); // Set up dynamic resource to test using raw values. - let gvk = GroupVersionKind::gvk("clux.dev", "v1", "Foo")?; + let gvk = GroupVersionKind::gvk("clux.dev", "v1", "Foo"); let api_resource = ApiResource::from_gvk(&gvk); let dynapi: Api = Api::namespaced_with(client.clone(), &namespace, &api_resource); diff --git a/examples/dynamic_watcher.rs b/examples/dynamic_watcher.rs index 6ba4d3545..ad5c42c3f 100644 --- a/examples/dynamic_watcher.rs +++ b/examples/dynamic_watcher.rs @@ -18,7 +18,7 @@ async fn main() -> anyhow::Result<()> { let kind = env::var("KIND").unwrap_or_else(|_| "Foo".into()); // Turn them into a GVK - let gvk = GroupVersionKind::gvk(&group, &version, &kind)?; + let gvk = GroupVersionKind::gvk(&group, &version, &kind); let api_resource = ApiResource::from_gvk(&gvk); // Use them in an Api with the GVK as its DynamicType let api = Api::::all_with(client, &api_resource); diff --git a/kube-runtime/src/reflector/object_ref.rs b/kube-runtime/src/reflector/object_ref.rs index 86fe6f61a..30e49a6aa 100644 --- a/kube-runtime/src/reflector/object_ref.rs +++ b/kube-runtime/src/reflector/object_ref.rs @@ -129,14 +129,6 @@ impl ObjectRef { } pub fn erase(self) -> ObjectRef { - let gvk = kube::api::GroupVersionKind::gvk( - K::group(&self.dyntype).as_ref(), - K::version(&self.dyntype).as_ref(), - K::kind(&self.dyntype).as_ref(), - ) - .expect("valid gvk"); - let mut res = kube::api::ApiResource::from_gvk(&gvk); - res.plural = K::plural(&self.dyntype).to_string(); ObjectRef { dyntype: kube::api::ApiResource::erase::(&self.dyntype), name: self.name, diff --git a/kube/src/api/dynamic.rs b/kube/src/api/dynamic.rs index e6a92d363..489660614 100644 --- a/kube/src/api/dynamic.rs +++ b/kube/src/api/dynamic.rs @@ -301,7 +301,7 @@ mod test { }; #[test] fn raw_custom_resource() { - let gvk = GroupVersionKind::gvk("clux.dev", "v1", "Foo").unwrap(); + let gvk = GroupVersionKind::gvk("clux.dev", "v1", "Foo"); let res = ApiResource::from_gvk(&gvk); let url = DynamicObject::url_path(&res, Some("myns")); @@ -318,7 +318,7 @@ mod test { #[test] fn raw_resource_in_default_group() -> Result<()> { - let gvk = GroupVersionKind::gvk("", "v1", "Service").unwrap(); + let gvk = GroupVersionKind::gvk("", "v1", "Service"); let api_resource = ApiResource::from_gvk(&gvk); let url = DynamicObject::url_path(&api_resource, None); let pp = PostParams::default(); @@ -342,7 +342,7 @@ mod test { } let client = Client::try_default().await.unwrap(); - let gvk = GroupVersionKind::gvk("clux.dev", "v1", "Foo").unwrap(); + let gvk = GroupVersionKind::gvk("clux.dev", "v1", "Foo"); let api_resource = ApiResource::from_gvk(&gvk); let a1: Api = Api::namespaced_with(client.clone(), "myns", &api_resource); let a2: Api = Api::namespaced(client.clone(), "myns"); diff --git a/kube/src/api/gvk.rs b/kube/src/api/gvk.rs index 587b7c772..9dacf4be4 100644 --- a/kube/src/api/gvk.rs +++ b/kube/src/api/gvk.rs @@ -1,7 +1,5 @@ -use crate::{Error, Result}; use serde::{Deserialize, Serialize}; - /// Contains enough information to identify API Resource. #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)] pub struct GroupVersionKind { @@ -15,27 +13,15 @@ pub struct GroupVersionKind { impl GroupVersionKind { /// Set the api group, version, and kind for a resource - pub fn gvk(group_: &str, version_: &str, kind_: &str) -> Result { + pub fn gvk(group_: &str, version_: &str, kind_: &str) -> Self { let version = version_.to_string(); let group = group_.to_string(); let kind = kind_.to_string(); - if version.is_empty() { - return Err(Error::DynamicType(format!( - "GroupVersionKind '{}' must have a version", - kind - ))); - } - if kind.is_empty() { - return Err(Error::DynamicType(format!( - "GroupVersionKind '{}' must have a kind", - kind - ))); - } - Ok(Self { group, version, kind }) + + Self { group, version, kind } } } - /// Represents a type-erased object resource. #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)] pub struct GroupVersionResource { @@ -52,7 +38,7 @@ pub struct GroupVersionResource { impl GroupVersionResource { /// Set the api group, version, and the plural resource name. - pub fn gvr(group_: &str, version_: &str, resource_: &str) -> Result { + pub fn gvr(group_: &str, version_: &str, resource_: &str) -> Self { let version = version_.to_string(); let group = group_.to_string(); let resource = resource_.to_string(); @@ -61,23 +47,12 @@ impl GroupVersionResource { } else { format!("{}/{}", group, version) }; - if version.is_empty() { - return Err(Error::DynamicType(format!( - "GroupVersionResource '{}' must have a version", - resource - ))); - } - if resource.is_empty() { - return Err(Error::DynamicType(format!( - "GroupVersionResource '{}' must have a resource", - resource - ))); - } - Ok(Self { + + Self { group, version, resource, api_version, - }) + } } } diff --git a/kube/src/error.rs b/kube/src/error.rs index 35d7136e4..d0192bba4 100644 --- a/kube/src/error.rs +++ b/kube/src/error.rs @@ -70,10 +70,6 @@ pub enum Error { #[error("Request validation failed with {0}")] RequestValidation(String), - /// A dynamic type conversion failure - #[error("Dynamic type conversion failed {0}")] - DynamicType(String), - /// Configuration error #[error("Error loading kubeconfig: {0}")] Kubeconfig(#[from] ConfigError), From 32b6083e59c7c6e5dd069c97d0639677e62af54a Mon Sep 17 00:00:00 2001 From: Mikail Bagishov Date: Mon, 19 Apr 2021 15:00:08 +0300 Subject: [PATCH 6/8] Update dynamic watcher --- examples/dynamic_watcher.rs | 12 +++++++++++- kube/src/api/gvk.rs | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/examples/dynamic_watcher.rs b/examples/dynamic_watcher.rs index ad5c42c3f..a903e8dd5 100644 --- a/examples/dynamic_watcher.rs +++ b/examples/dynamic_watcher.rs @@ -19,7 +19,17 @@ async fn main() -> anyhow::Result<()> { // Turn them into a GVK let gvk = GroupVersionKind::gvk(&group, &version, &kind); - let api_resource = ApiResource::from_gvk(&gvk); + let mut api_resource = ApiResource::from_gvk(&gvk); + + if let Some(resource) = env::var("RESOURCE").ok() { + api_resource.plural = resource; + } else { + println!( + "Using inferred plural name (use RESOURCE to override): {}", + api_resource.plural + ); + } + // Use them in an Api with the GVK as its DynamicType let api = Api::::all_with(client, &api_resource); diff --git a/kube/src/api/gvk.rs b/kube/src/api/gvk.rs index 9dacf4be4..be61b664d 100644 --- a/kube/src/api/gvk.rs +++ b/kube/src/api/gvk.rs @@ -47,7 +47,7 @@ impl GroupVersionResource { } else { format!("{}/{}", group, version) }; - + Self { group, version, From 62f4d0ab5e56b38685aee78aef87e80e9878a593 Mon Sep 17 00:00:00 2001 From: Mikail Bagishov Date: Sat, 24 Apr 2021 01:59:04 +0300 Subject: [PATCH 7/8] finish rebase --- examples/dynamic_api.rs | 33 +++--- kube/src/api/dynamic.rs | 112 +-------------------- kube/src/api/mod.rs | 2 +- kube/src/client/discovery.rs | 188 +++++++++++++++++++++++++++-------- kube/src/client/mod.rs | 2 +- 5 files changed, 160 insertions(+), 177 deletions(-) diff --git a/examples/dynamic_api.rs b/examples/dynamic_api.rs index 05954483a..5baaff496 100644 --- a/examples/dynamic_api.rs +++ b/examples/dynamic_api.rs @@ -2,10 +2,10 @@ //! to `kubectl get all --all-namespaces`. use kube::{ - api::{Api, ApiResource, DynamicObject, Resource, ResourceExt}, - client::{Client, Discovery}, + api::{Api, DynamicObject, ResourceExt}, + client::{Client, Discovery, Scope}, }; -use log::{info, warn}; +use log::info; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -22,30 +22,23 @@ async fn main() -> anyhow::Result<()> { for group in discovery.groups() { let ver = group.preferred_version_or_guess(); - for gvk in group.resources_by_version(ver) { - let kind = DynamicObject::kind(&gvk); - let (_, raw_resource) = discovery - .resolve_group_version_kind(group.name(), ver, &kind) - .unwrap(); - let api: Api = if raw_resource.namespaced { + for (api_res, extras) in group.resources_by_version(ver) { + if !extras.operations.list { + continue; + } + let api: Api = if extras.scope == Scope::Namespaced { if let Some(ns) = &ns_filter { - Api::namespaced_with(client.clone(), ns, &gvk) + Api::namespaced_with(client.clone(), ns, &api_res) } else { - Api::all_with(client.clone(), &gvk) + Api::all_with(client.clone(), &api_res) } } else { - Api::all_with(client.clone(), &gvk) + Api::all_with(client.clone(), &api_res) }; - info!("{}/{} : {}", group.name(), ver, kind); + info!("{}/{} : {}", group.name(), ver, api_res.kind); - let list = match api.list(&Default::default()).await { - Ok(l) => l, - Err(e) => { - warn!("Failed to list: {:#}", e); - continue; - } - }; + let list = api.list(&Default::default()).await?; for item in list.items { let name = item.name(); let ns = item.metadata.namespace.map(|s| s + "/").unwrap_or_default(); diff --git a/kube/src/api/dynamic.rs b/kube/src/api/dynamic.rs index 489660614..8ecb0d236 100644 --- a/kube/src/api/dynamic.rs +++ b/kube/src/api/dynamic.rs @@ -1,5 +1,5 @@ use crate::api::{metadata::TypeMeta, GroupVersionKind, Resource}; -use k8s_openapi::apimachinery::pkg::apis::meta::v1::{APIResource, APIResourceList, ObjectMeta}; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::{APIResource, ObjectMeta}; use std::borrow::Cow; /// Contains information about Kubernetes API resources @@ -181,116 +181,6 @@ impl Resource for DynamicObject { } } -/// Resource scope -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub enum Scope { - /// Objects are global - Cluster, - /// Each object lives in namespace. - Namespaced, -} - -/// Operations that are supported on the resource -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct Operations { - /// Object can be created - pub create: bool, - /// Single object can be queried - pub get: bool, - /// Multiple objects can be queried - pub list: bool, - /// A watch can be started - pub watch: bool, - /// A single object can be deleted - pub delete: bool, - /// Multiple objects can be deleted - pub delete_collection: bool, - /// Object can be updated - pub update: bool, - /// Object can be patched - pub patch: bool, - /// All other verbs - pub other: Vec, -} - -impl Operations { - /// Returns empty `Operations` - pub fn empty() -> Self { - Operations { - create: false, - get: false, - list: false, - watch: false, - delete: false, - delete_collection: false, - update: false, - patch: false, - other: Vec::new(), - } - } -} -/// Contains additional, detailed information abount API resource -pub struct ApiResourceExtras { - /// 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. - pub subresources: Vec<(ApiResource, ApiResourceExtras)>, - /// Supported operations on this resource - pub operations: Operations, -} - -impl ApiResourceExtras { - /// Creates ApiResourceExtras from `meta::v1::APIResourceList` instance. - /// This function correctly sets all fields except `subresources`. - /// # Panics - /// Panics if list does not contain resource named `name`. - pub fn from_apiresourcelist(list: &APIResourceList, name: &str) -> Self { - let ar = list - .resources - .iter() - .find(|r| r.name == name) - .expect("resource not found in APIResourceList"); - let scope = if ar.namespaced { - Scope::Namespaced - } else { - Scope::Cluster - }; - let mut operations = Operations::empty(); - for verb in &ar.verbs { - match verb.as_str() { - "create" => operations.create = true, - "get" => operations.get = true, - "list" => operations.list = true, - "watch" => operations.watch = true, - "delete" => operations.delete = true, - "deletecollection" => operations.delete_collection = true, - "update" => operations.update = true, - "patch" => operations.patch = true, - _ => operations.other.push(verb.clone()), - } - } - let mut subresources = Vec::new(); - let subresource_name_prefix = format!("{}/", name); - for res in &list.resources { - if let Some(subresource_name) = res.name.strip_prefix(&subresource_name_prefix) { - let mut api_resource = ApiResource::from_apiresource(res, &list.group_version); - api_resource.plural = subresource_name.to_string(); - let extra = ApiResourceExtras::from_apiresourcelist(list, &res.name); - subresources.push((api_resource, extra)); - } - } - - ApiResourceExtras { - scope, - subresources, - operations, - } - } -} - #[cfg(test)] mod test { use crate::{ diff --git a/kube/src/api/mod.rs b/kube/src/api/mod.rs index f6507e526..2b1b7a8fb 100644 --- a/kube/src/api/mod.rs +++ b/kube/src/api/mod.rs @@ -23,7 +23,7 @@ mod gvk; pub use gvk::{GroupVersionKind, GroupVersionResource}; mod dynamic; -pub use dynamic::{ApiResource, ApiResourceExtras, DynamicObject, Operations, Scope}; +pub use dynamic::{ApiResource, DynamicObject}; #[cfg(feature = "ws")] mod remote_command; #[cfg(feature = "ws")] pub use remote_command::AttachedProcess; diff --git a/kube/src/client/discovery.rs b/kube/src/client/discovery.rs index aaefc8b19..a8f26531f 100644 --- a/kube/src/client/discovery.rs +++ b/kube/src/client/discovery.rs @@ -1,19 +1,142 @@ -use crate::{api::GroupVersionKind, Client}; -use k8s_openapi::apimachinery::pkg::apis::meta::v1::{APIResource, APIResourceList}; +use crate::{api::ApiResource, Client}; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::APIResourceList; use std::{cmp::Reverse, collections::HashMap}; +/// Resource scope +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub enum Scope { + /// Objects are global + Cluster, + /// Each object lives in namespace. + Namespaced, +} + +/// Operations that are supported on the resource +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct Operations { + /// Object can be created + pub create: bool, + /// Single object can be queried + pub get: bool, + /// Multiple objects can be queried + pub list: bool, + /// A watch can be started + pub watch: bool, + /// A single object can be deleted + pub delete: bool, + /// Multiple objects can be deleted + pub delete_collection: bool, + /// Object can be updated + pub update: bool, + /// Object can be patched + pub patch: bool, + /// All other verbs + pub other: Vec, +} + +impl Operations { + /// Returns empty `Operations` + pub fn empty() -> Self { + Operations { + create: false, + get: false, + list: false, + watch: false, + delete: false, + delete_collection: false, + update: false, + patch: false, + other: Vec::new(), + } + } +} +/// Contains additional, detailed information abount API resource +#[derive(Debug, Clone)] +pub struct ApiResourceExtras { + /// 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. + pub subresources: Vec<(ApiResource, ApiResourceExtras)>, + /// Supported operations on this resource + pub operations: Operations, +} + +impl ApiResourceExtras { + /// Creates ApiResourceExtras from `meta::v1::APIResourceList` instance. + /// This function correctly sets all fields except `subresources`. + /// # Panics + /// Panics if list does not contain resource named `name`. + pub fn from_apiresourcelist(list: &APIResourceList, name: &str) -> Self { + let ar = list + .resources + .iter() + .find(|r| r.name == name) + .expect("resource not found in APIResourceList"); + let scope = if ar.namespaced { + Scope::Namespaced + } else { + Scope::Cluster + }; + let mut operations = Operations::empty(); + for verb in &ar.verbs { + match verb.as_str() { + "create" => operations.create = true, + "get" => operations.get = true, + "list" => operations.list = true, + "watch" => operations.watch = true, + "delete" => operations.delete = true, + "deletecollection" => operations.delete_collection = true, + "update" => operations.update = true, + "patch" => operations.patch = true, + _ => operations.other.push(verb.clone()), + } + } + let mut subresources = Vec::new(); + let subresource_name_prefix = format!("{}/", name); + for res in &list.resources { + if let Some(subresource_name) = res.name.strip_prefix(&subresource_name_prefix) { + let mut api_resource = ApiResource::from_apiresource(res, &list.group_version); + api_resource.plural = subresource_name.to_string(); + let extra = ApiResourceExtras::from_apiresourcelist(list, &res.name); + subresources.push((api_resource, extra)); + } + } + + ApiResourceExtras { + scope, + subresources, + operations, + } + } +} + struct GroupVersionData { version: String, - list: APIResourceList, - resources: Vec, + // list: APIResourceList, + resources: Vec<(ApiResource, ApiResourceExtras)>, } impl GroupVersionData { fn new(version: String, list: APIResourceList) -> Self { + // TODO: could be better than O(N^2). + let mut resources = Vec::new(); + for res in &list.resources { + // skip subresources + if res.name.contains('/') { + continue; + } + let api_res = ApiResource::from_apiresource(res, &list.group_version); + let extra = ApiResourceExtras::from_apiresourcelist(&list, &res.name); + resources.push((api_res, extra)); + } GroupVersionData { version, - list: list.clone(), - resources: filter_api_resource_list(list), + resources, + //list: list.clone(), + // resources: filter_api_resource_list(list), } } } @@ -30,17 +153,17 @@ pub struct Group { /// On creation `Discovery` queries Kubernetes API, /// making list of all API resources, and provides a simple /// interface on the top of that information. +/// +/// # Resource representation +/// Each resource is represented as a pair +/// `(ApiResource, ApiResourceExtras)`. Former can be used +/// to make API requests (together with the `DynamicObject` +/// or `Object`). Latter provides additional information such +/// as scope or supported verbs. pub struct Discovery { groups: HashMap, } -fn filter_api_resource_list(resource_list: APIResourceList) -> Vec { - let mut resource_list = resource_list.resources; - // skip subresources - resource_list.retain(|ar| !ar.name.contains('/')); - resource_list -} - // TODO: this is pretty unoptimized impl Discovery { /// Discovers all APIs available in the cluster, @@ -118,29 +241,20 @@ impl Discovery { /// Returns resource with given group, version and kind. /// - /// This function returns `GroupVersionKind` which can be used together - /// with `DynamicObject` and raw `APIResource` value with additional information. + /// This function returns `ApiResource` which can be used together + /// with `DynamicObject` and raw `ApiResourceExtras` value with additional information. pub fn resolve_group_version_kind( &self, group: &str, version: &str, kind: &str, - ) -> Option<(GroupVersionKind, APIResource)> { + ) -> Option<(ApiResource, ApiResourceExtras)> { // TODO: could be better than O(N) let group = self.group(group)?; group .resources_by_version(version) .into_iter() - .find(|gvk| gvk.kind == kind) - .map(|gvk| { - let data = group - .versions_and_resources - .iter() - .find(|data| data.version == version) - .unwrap(); - let raw = data.list.resources.iter().find(|r| r.kind == kind).unwrap(); - (gvk, raw.clone()) - }) + .find(|res| res.0.kind == kind) } } @@ -176,13 +290,13 @@ impl Group { /// Returns preferred version for working with given group. pub fn preferred_version(&self) -> Option<&str> { - self.preferred_version.as_deref() + self.preferred_version.as_deref() } /// Returns preferred version for working with given group. /// If server does not recommend one, this function picks /// "the most stable and the most recent" version. - + pub fn preferred_version_or_guess(&self) -> &str { match &self.preferred_version { Some(v) => v, @@ -193,28 +307,14 @@ impl Group { /// Returns resources available in version `ver` of this group. /// If the group does not support this version, /// returns empty vector. - pub fn resources_by_version(&self, ver: &str) -> Vec { + pub fn resources_by_version(&self, ver: &str) -> Vec<(ApiResource, ApiResourceExtras)> { let resources = self .versions_and_resources .iter() .find(|ver_data| ver_data.version == ver) .map(|ver_data| ver_data.resources.as_slice()) .unwrap_or(&[]); - resources - .iter() - .cloned() - .map(|mut api_resource| { - api_resource.group = Some(if self.name == "core" { - String::new() - } else { - self.name.clone() - }); - api_resource.version = Some(ver.to_string()); - // second argument will be ignored because we have just filled necessary - // `api_resource` fields. - GroupVersionKind::from_api_resource(&api_resource, "unused/v0") - }) - .collect() + resources.iter().cloned().collect() } } diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index 2be14554c..3326bd6de 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -7,7 +7,7 @@ //! interaction with the kuberneres API. mod discovery; -pub use discovery::{Discovery, Group}; +pub use discovery::{ApiResourceExtras, Discovery, Group, Operations, Scope}; use crate::{api::WatchEvent, config::Config, error::ErrorResponse, service::Service, Error, Result}; From 053ae2771528b7386f3a890e12368d41cdf3237c Mon Sep 17 00:00:00 2001 From: Mikail Bagishov Date: Mon, 26 Apr 2021 01:19:15 +0300 Subject: [PATCH 8/8] Remove Operations --- examples/dynamic_api.rs | 7 ++- kube/src/client/discovery.rs | 82 ++++++++++++------------------------ kube/src/client/mod.rs | 4 +- 3 files changed, 34 insertions(+), 59 deletions(-) diff --git a/examples/dynamic_api.rs b/examples/dynamic_api.rs index 5baaff496..09c7c8487 100644 --- a/examples/dynamic_api.rs +++ b/examples/dynamic_api.rs @@ -3,7 +3,10 @@ use kube::{ api::{Api, DynamicObject, ResourceExt}, - client::{Client, Discovery, Scope}, + client::{ + discovery::{verbs, Discovery, Scope}, + Client, + }, }; use log::info; @@ -23,7 +26,7 @@ async fn main() -> anyhow::Result<()> { for group in discovery.groups() { let ver = group.preferred_version_or_guess(); for (api_res, extras) in group.resources_by_version(ver) { - if !extras.operations.list { + if !extras.supports_operation(verbs::LIST) { continue; } let api: Api = if extras.scope == Scope::Namespaced { diff --git a/kube/src/client/discovery.rs b/kube/src/client/discovery.rs index a8f26531f..705c04dbc 100644 --- a/kube/src/client/discovery.rs +++ b/kube/src/client/discovery.rs @@ -1,3 +1,5 @@ +//! High-level utilities for runtime API discovery. + use crate::{api::ApiResource, Client}; use k8s_openapi::apimachinery::pkg::apis::meta::v1::APIResourceList; use std::{cmp::Reverse, collections::HashMap}; @@ -11,45 +13,26 @@ pub enum Scope { Namespaced, } -/// Operations that are supported on the resource -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct Operations { - /// Object can be created - pub create: bool, - /// Single object can be queried - pub get: bool, - /// Multiple objects can be queried - pub list: bool, - /// A watch can be started - pub watch: bool, - /// A single object can be deleted - pub delete: bool, - /// Multiple objects can be deleted - pub delete_collection: bool, - /// Object can be updated - pub update: bool, - /// Object can be patched - pub patch: bool, - /// All other verbs - pub other: Vec, +/// Defines standard verbs +pub mod verbs { + /// Create a resource + pub const CREATE: &'static str = "create"; + /// Get single resource + pub const GET: &'static str = "get"; + /// List objects + pub const LIST: &'static str = "list"; + /// Watch for objects changes + pub const WATCH: &'static str = "watch"; + /// Delete single object + pub const DELETE: &'static str = "delete"; + /// Delete multiple objects at once + pub const DELETE_COLLECTION: &'static str = "deletecollection"; + /// Update an object + pub const UPDATE: &'static str = "update"; + /// Patch an object + pub const PATCH: &'static str = "patch"; } -impl Operations { - /// Returns empty `Operations` - pub fn empty() -> Self { - Operations { - create: false, - get: false, - list: false, - watch: false, - delete: false, - delete_collection: false, - update: false, - patch: false, - other: Vec::new(), - } - } -} /// Contains additional, detailed information abount API resource #[derive(Debug, Clone)] pub struct ApiResourceExtras { @@ -61,7 +44,7 @@ pub struct ApiResourceExtras { /// To work with subresources, use `Request` methods. pub subresources: Vec<(ApiResource, ApiResourceExtras)>, /// Supported operations on this resource - pub operations: Operations, + pub operations: Vec, } impl ApiResourceExtras { @@ -80,20 +63,6 @@ impl ApiResourceExtras { } else { Scope::Cluster }; - let mut operations = Operations::empty(); - for verb in &ar.verbs { - match verb.as_str() { - "create" => operations.create = true, - "get" => operations.get = true, - "list" => operations.list = true, - "watch" => operations.watch = true, - "delete" => operations.delete = true, - "deletecollection" => operations.delete_collection = true, - "update" => operations.update = true, - "patch" => operations.patch = true, - _ => operations.other.push(verb.clone()), - } - } let mut subresources = Vec::new(); let subresource_name_prefix = format!("{}/", name); for res in &list.resources { @@ -108,9 +77,14 @@ impl ApiResourceExtras { ApiResourceExtras { scope, subresources, - operations, + operations: ar.verbs.clone(), } } + + /// Checks that given verb is supported on this resource. + pub fn supports_operation(&self, operation: &str) -> bool { + self.operations.iter().any(|op| op == operation) + } } struct GroupVersionData { @@ -148,7 +122,7 @@ pub struct Group { preferred_version: Option, } -/// High-level utility for runtime API discovery. +/// Cached APIs information. /// /// On creation `Discovery` queries Kubernetes API, /// making list of all API resources, and provides a simple diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index 3326bd6de..5c8616c9d 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -5,9 +5,7 @@ //! This client can be used on its own or in conjuction with //! the [`Api`][crate::api::Api] type for more structured //! interaction with the kuberneres API. -mod discovery; - -pub use discovery::{ApiResourceExtras, Discovery, Group, Operations, Scope}; +pub mod discovery; use crate::{api::WatchEvent, config::Config, error::ErrorResponse, service::Service, Error, Result};