diff --git a/examples/crd_derive.rs b/examples/crd_derive.rs index 00ab5875c..b3c409d37 100644 --- a/examples/crd_derive.rs +++ b/examples/crd_derive.rs @@ -1,5 +1,5 @@ -use k8s_openapi::{apimachinery::pkg::apis::meta::v1::Condition, Resource}; -use kube::CustomResource; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::Condition; +use kube::{CustomResource, Resource}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; group = "clux.dev", version = "v1", kind = "Foo", + plural = "fooz", struct = "FooCrd", namespaced, status = "FooStatus", @@ -35,7 +36,7 @@ pub struct FooStatus { } fn main() { - println!("Kind {}", FooCrd::KIND); + println!("Kind {}", FooCrd::kind(&())); let mut foo = FooCrd::new("hi", MyFoo { name: "hi".into(), info: None, @@ -84,13 +85,13 @@ fn verify_crd() { "apiVersion": "apiextensions.k8s.io/v1", "kind": "CustomResourceDefinition", "metadata": { - "name": "foos.clux.dev" + "name": "fooz.clux.dev" }, "spec": { "group": "clux.dev", "names": { "kind": "Foo", - "plural": "foos", + "plural": "fooz", "shortNames": ["f"], "singular": "foo" }, @@ -183,20 +184,29 @@ fn verify_crd() { } }); let crd = serde_json::to_value(FooCrd::crd()).unwrap(); + println!("got crd: {}", serde_yaml::to_string(&FooCrd::crd()).unwrap()); assert_eq!(crd, output); } #[test] fn verify_resource() { use static_assertions::{assert_impl_all, assert_impl_one}; - assert_eq!(FooCrd::KIND, "Foo"); - assert_eq!(FooCrd::GROUP, "clux.dev"); - assert_eq!(FooCrd::VERSION, "v1"); - assert_eq!(FooCrd::API_VERSION, "clux.dev/v1"); - assert_impl_all!(FooCrd: k8s_openapi::Resource, k8s_openapi::Metadata, Default); + assert_eq!(FooCrd::kind(&()), "Foo"); + assert_eq!(FooCrd::group(&()), "clux.dev"); + assert_eq!(FooCrd::version(&()), "v1"); + assert_eq!(FooCrd::api_version(&()), "clux.dev/v1"); + assert_impl_all!(FooCrd: Resource, Default); assert_impl_one!(MyFoo: JsonSchema); } +#[tokio::test] +async fn verify_api_gen() { + use kube::{Api, Client}; + let client = Client::try_default().await.unwrap(); + let api: Api = Api::namespaced(client, "myns"); + assert_eq!(api.resource_url(), "/apis/clux.dev/v1/namespaces/myns/fooz"); +} + #[test] fn verify_default() { let fdef = FooCrd::default(); @@ -206,6 +216,7 @@ apiVersion: clux.dev/v1 kind: Foo metadata: {} spec: - name: """#; + name: "" +"#; assert_eq!(exp, ser); } diff --git a/examples/crd_derive_no_schema.rs b/examples/crd_derive_no_schema.rs index 6db64b57f..ee66c656b 100644 --- a/examples/crd_derive_no_schema.rs +++ b/examples/crd_derive_no_schema.rs @@ -60,14 +60,14 @@ fn main() { #[cfg(not(feature = "schema"))] #[test] fn verify_bar_is_a_custom_resource() { - use k8s_openapi::Resource; + use kube::Resource; use schemars::JsonSchema; // only for ensuring it's not implemented use static_assertions::{assert_impl_all, assert_not_impl_any}; - println!("Kind {}", Bar::KIND); + println!("Kind {}", Bar::kind(&())); let bar = Bar::new("five", MyBar { bars: 5 }); println!("Spec: {:?}", bar.spec); - assert_impl_all!(Bar: k8s_openapi::Resource, k8s_openapi::Metadata); + assert_impl_all!(Bar: kube::Resource); assert_not_impl_any!(MyBar: JsonSchema); // but no schemars schema implemented let crd = Bar::crd_with_manual_schema(); diff --git a/kube-derive/Cargo.toml b/kube-derive/Cargo.toml index adf5ad1ad..82b2598af 100644 --- a/kube-derive/Cargo.toml +++ b/kube-derive/Cargo.toml @@ -28,6 +28,7 @@ schema = [] [dev-dependencies] serde = { version = "1.0.118", features = ["derive"] } serde_yaml = "0.8.17" +kube = { path = "../kube", version = "^0.51.0"} k8s-openapi = { version = "0.11.0", default-features = false, features = ["v1_20"] } schemars = { version = "0.8.0", features = ["chrono"] } chrono = "0.4.19" diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index 065c4bf3d..dd138ca61 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -164,8 +164,8 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea impl #rootident { pub fn new(name: &str, spec: #ident) -> Self { Self { - api_version: <#rootident as k8s_openapi::Resource>::API_VERSION.to_string(), - kind: <#rootident as k8s_openapi::Resource>::KIND.to_string(), + api_version: <#rootident as kube::Resource>::api_version(&()).to_string(), + kind: <#rootident as kube::Resource>::kind(&()).to_string(), metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { name: Some(name.to_string()), ..Default::default() @@ -177,37 +177,62 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea } }; - // 2. Implement Resource trait for k8s_openapi + // 2. Implement Resource trait + let name = singular.unwrap_or_else(|| kind.to_ascii_lowercase()); + let plural = plural.unwrap_or_else(|| to_plural(&name)); + let scope = if namespaced { "Namespaced" } else { "Cluster" }; + let api_ver = format!("{}/{}", group, version); let impl_resource = quote! { - impl k8s_openapi::Resource for #rootident { - const API_VERSION: &'static str = #api_ver; - const GROUP: &'static str = #group; - const KIND: &'static str = #kind; - const VERSION: &'static str = #version; - } - }; + impl kube::Resource for #rootident { + type DynamicType = (); - // 3. Implement Metadata trait for k8s_openapi - let impl_metadata = quote! { - impl k8s_openapi::Metadata for #rootident { - type Ty = k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; - fn metadata(&self) -> &Self::Ty { + fn group(_: &()) -> std::borrow::Cow<'_, str> { + #group.into() + } + + fn kind(_: &()) -> std::borrow::Cow<'_, str> { + #kind.into() + } + + fn version(_: &()) -> std::borrow::Cow<'_, str> { + #version.into() + } + + fn api_version(_: &()) -> std::borrow::Cow<'_, str> { + #api_ver.into() + } + + fn plural(_: &()) -> std::borrow::Cow<'_, str> { + #plural.into() + } + + fn meta(&self) -> &kube::api::ObjectMeta { &self.metadata } - fn metadata_mut(&mut self) -> &mut Self::Ty { - &mut self.metadata + + fn name(&self) -> String { + self.meta().name.clone().expect("kind has metadata.name") + } + + fn resource_ver(&self) -> Option { + self.meta().resource_version.clone() + } + + fn namespace(&self) -> Option { + self.meta().namespace.clone() } } }; - // 4. Implement Default if requested + + // 3. Implement Default if requested let impl_default = if has_default { quote! { impl Default for #rootident { fn default() -> Self { Self { - api_version: <#rootident as k8s_openapi::Resource>::API_VERSION.to_string(), - kind: <#rootident as k8s_openapi::Resource>::KIND.to_string(), + api_version: <#rootident as kube::Resource>::api_version(&()).to_string(), + kind: <#rootident as kube::Resource>::kind(&()).to_string(), metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta::default(), spec: Default::default(), #statusdef @@ -219,10 +244,7 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea quote! {} }; - // 5. Implement CustomResource - let name = singular.unwrap_or_else(|| kind.to_ascii_lowercase()); - let plural = plural.unwrap_or_else(|| to_plural(&name)); - let scope = if namespaced { "Namespaced" } else { "Cluster" }; + // 4. Implement CustomResource // Compute a bunch of crd props let mut printers = format!("[ {} ]", printcolums.join(",")); // hacksss @@ -315,7 +337,7 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea } }; - // TODO: should ::crd be from a trait? + // Implement the ::crd method (fine to not have in a trait as its a generated type) let impl_crd = quote! { impl #rootident { pub fn crd() -> #apiext::CustomResourceDefinition { @@ -350,7 +372,6 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea quote! { #root_obj #impl_resource - #impl_metadata #impl_default #impl_crd } diff --git a/kube-derive/src/lib.rs b/kube-derive/src/lib.rs index bd71fb161..f41f2ae04 100644 --- a/kube-derive/src/lib.rs +++ b/kube-derive/src/lib.rs @@ -10,8 +10,8 @@ mod custom_resource; /// A custom derive for kubernetes custom resource definitions. /// /// This will generate a **root object** containing your spec and metadata. -/// This root object will implement the [`k8s_openapi::Metadata`] + [`k8s_openapi::Resource`] -/// traits so it can be used with [`kube::Api`]. +/// This root object will implement the [`kube::Resource`] trait +/// so it can be used with [`kube::Api`]. /// /// The generated type will also implement a `::crd` method to generate the crd /// at the specified api version (or `v1` if unspecified). @@ -20,7 +20,7 @@ mod custom_resource; /// /// ```rust /// use serde::{Serialize, Deserialize}; -/// use k8s_openapi::Resource; +/// use kube::Resource; /// use kube_derive::CustomResource; /// use schemars::JsonSchema; /// @@ -30,7 +30,7 @@ mod custom_resource; /// info: String, /// } /// -/// println!("kind = {}", Foo::KIND); // impl k8s_openapi::Resource +/// println!("kind = {}", Foo::kind(&())); // impl kube::Resource /// let f = Foo::new("foo-1", FooSpec { /// info: "informative info".into(), /// }); @@ -149,8 +149,7 @@ mod custom_resource; /// spec: FooSpec, /// status: Option, /// } -/// impl k8s_openapi::Resource for FooCrd {...} -/// impl k8s_openapi::Metadata for FooCrd {...} +/// impl kube::Resource for FooCrd {...} /// /// impl FooCrd { /// pub fn new(name: &str, spec: FooSpec) -> Self { ... } @@ -196,8 +195,7 @@ mod custom_resource; /// /// [`kube`]: https://docs.rs/kube /// [`kube::Api`]: https://docs.rs/kube/*/kube/struct.Api.html -/// [`k8s_openapi::Metadata`]: https://docs.rs/k8s-openapi/*/k8s_openapi/trait.Metadata.html -/// [`k8s_openapi::Resource`]: https://docs.rs/k8s-openapi/*/k8s_openapi/trait.Resource.html +/// [`kube::Resource`]: https://docs.rs/kube/*/kube/trait.Resource.html #[proc_macro_derive(CustomResource, attributes(kube))] pub fn derive_custom_resource(input: proc_macro::TokenStream) -> proc_macro::TokenStream { custom_resource::derive(proc_macro2::TokenStream::from(input)).into() diff --git a/kube/src/api/dynamic.rs b/kube/src/api/dynamic.rs index ebc01b0a1..55c994496 100644 --- a/kube/src/api/dynamic.rs +++ b/kube/src/api/dynamic.rs @@ -16,6 +16,8 @@ pub struct GroupVersionKind { kind: String, /// Concatenation of group and version api_version: String, + /// Optional plural/resource + plural: Option, } impl GroupVersionKind { @@ -52,11 +54,13 @@ impl GroupVersionKind { } else { format!("{}/{}", group, version) }; + let plural = Some(ar.name.clone()); Self { group, version, kind, api_version, + plural, } } @@ -87,8 +91,15 @@ impl GroupVersionKind { 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 + } } /// A dynamic representation of a kubernetes resource @@ -139,20 +150,29 @@ impl DynamicObject { impl Resource for DynamicObject { type DynamicType = GroupVersionKind; - fn group(f: &GroupVersionKind) -> Cow<'_, str> { - f.group.as_str().into() + fn group(dt: &GroupVersionKind) -> Cow<'_, str> { + dt.group.as_str().into() } - fn version(f: &GroupVersionKind) -> Cow<'_, str> { - f.version.as_str().into() + fn version(dt: &GroupVersionKind) -> Cow<'_, str> { + dt.version.as_str().into() } - fn kind(f: &GroupVersionKind) -> Cow<'_, str> { - f.kind.as_str().into() + fn kind(dt: &GroupVersionKind) -> Cow<'_, str> { + dt.kind.as_str().into() } - fn api_version(f: &GroupVersionKind) -> Cow<'_, str> { - f.api_version.as_str().into() + fn api_version(dt: &GroupVersionKind) -> 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 meta(&self) -> &ObjectMeta { @@ -208,6 +228,7 @@ mod test { #[tokio::test] #[ignore] // circle has no kubeconfig async fn convenient_custom_resource() { + use crate as kube; // derive macro needs kube in scope use crate::{Api, Client, CustomResource}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/kube/src/api/metadata.rs b/kube/src/api/metadata.rs index 907b99129..f424c61da 100644 --- a/kube/src/api/metadata.rs +++ b/kube/src/api/metadata.rs @@ -25,33 +25,45 @@ pub trait Resource { type DynamicType: Send + Sync + 'static; /// Returns kind of this object - fn kind(f: &Self::DynamicType) -> Cow<'_, str>; + fn kind(dt: &Self::DynamicType) -> Cow<'_, str>; /// Returns group of this object - fn group(f: &Self::DynamicType) -> Cow<'_, str>; + fn group(dt: &Self::DynamicType) -> Cow<'_, str>; /// Returns version of this object - fn version(f: &Self::DynamicType) -> Cow<'_, str>; + fn version(dt: &Self::DynamicType) -> Cow<'_, str>; /// Returns apiVersion of this object - fn api_version(f: &Self::DynamicType) -> Cow<'_, str> { - let group = Self::group(f); + fn api_version(dt: &Self::DynamicType) -> Cow<'_, str> { + let group = Self::group(dt); if group.is_empty() { - return Self::version(f); + return Self::version(dt); } let mut group = group.into_owned(); group.push('/'); - group.push_str(&Self::version(f)); + group.push_str(&Self::version(dt)); group.into() } + /// Returns the plural name of the kind + /// + /// This is known as the resource in apimachinery, we rename it for disambiguation. + /// By default, we infer this name through pluralization. + /// + /// The pluralization process is not recommended to be relied upon, and is only used for + /// `k8s_openapi` types, where we maintain a list of special pluralisations for compatibility. + /// + /// Thus when used with `DynamicObject` or `kube-derive`, we override this with correct values. + fn plural(dt: &Self::DynamicType) -> Cow<'_, str> { + to_plural(&Self::kind(dt).to_ascii_lowercase()).into() + } /// Creates a url path for http requests for this resource - fn url_path(t: &Self::DynamicType, namespace: Option<&str>) -> String { + fn url_path(dt: &Self::DynamicType, namespace: Option<&str>) -> String { let n = if let Some(ns) = namespace { format!("namespaces/{}/", ns) } else { "".into() }; - let group = Self::group(t); - let api_version = Self::api_version(t); - let plural = to_plural(&Self::kind(t).to_ascii_lowercase()); + let group = Self::group(dt); + let api_version = Self::api_version(dt); + let plural = Self::plural(dt); format!( "/{group}/{api_version}/{namespaces}{plural}", group = if group.is_empty() { "api" } else { "apis" }, @@ -78,15 +90,15 @@ where { type DynamicType = (); - fn kind<'a>(_: &()) -> Cow<'_, str> { + fn kind(_: &()) -> Cow<'_, str> { K::KIND.into() } - fn group<'a>(_: &()) -> Cow<'_, str> { + fn group(_: &()) -> Cow<'_, str> { K::GROUP.into() } - fn version<'a>(_: &()) -> Cow<'_, str> { + fn version(_: &()) -> Cow<'_, str> { K::VERSION.into() } @@ -125,7 +137,7 @@ pub struct TypeMeta { } // Simple pluralizer. Handles the special cases. -fn to_plural(word: &str) -> String { +pub(crate) fn to_plural(word: &str) -> String { if word == "endpoints" || word == "endpointslices" { return word.to_owned(); } else if word == "nodemetrics" { diff --git a/kube/src/api/typed.rs b/kube/src/api/typed.rs index a5cb08f7c..f68490734 100644 --- a/kube/src/api/typed.rs +++ b/kube/src/api/typed.rs @@ -89,6 +89,11 @@ impl Api { pub fn into_client(self) -> Client { self.into() } + + /// Return a reference to the current resource url path + pub fn resource_url(&self) -> &str { + &self.request.url_path + } } /// PUSH/PUT/POST/GET abstractions