Skip to content

Commit

Permalink
Merge pull request #481 from clux/plural-resource
Browse files Browse the repository at this point in the history
add plural to Resource and GroupVersionKind
  • Loading branch information
clux authored Mar 29, 2021
2 parents 40c8611 + 8876644 commit 0cadf85
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 71 deletions.
33 changes: 22 additions & 11 deletions examples/crd_derive.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};
group = "clux.dev",
version = "v1",
kind = "Foo",
plural = "fooz",
struct = "FooCrd",
namespaced,
status = "FooStatus",
Expand All @@ -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,
Expand Down Expand Up @@ -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"
},
Expand Down Expand Up @@ -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<FooCrd> = Api::namespaced(client, "myns");
assert_eq!(api.resource_url(), "/apis/clux.dev/v1/namespaces/myns/fooz");
}

#[test]
fn verify_default() {
let fdef = FooCrd::default();
Expand All @@ -206,6 +216,7 @@ apiVersion: clux.dev/v1
kind: Foo
metadata: {}
spec:
name: """#;
name: ""
"#;
assert_eq!(exp, ser);
}
6 changes: 3 additions & 3 deletions examples/crd_derive_no_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions kube-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
73 changes: 47 additions & 26 deletions kube-derive/src/custom_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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<String> {
self.meta().resource_version.clone()
}

fn namespace(&self) -> Option<String> {
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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
14 changes: 6 additions & 8 deletions kube-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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;
///
Expand All @@ -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(),
/// });
Expand Down Expand Up @@ -149,8 +149,7 @@ mod custom_resource;
/// spec: FooSpec,
/// status: Option<FooStatus>,
/// }
/// 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 { ... }
Expand Down Expand Up @@ -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()
Expand Down
37 changes: 29 additions & 8 deletions kube/src/api/dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub struct GroupVersionKind {
kind: String,
/// Concatenation of group and version
api_version: String,
/// Optional plural/resource
plural: Option<String>,
}

impl GroupVersionKind {
Expand Down Expand Up @@ -52,11 +54,13 @@ impl GroupVersionKind {
} else {
format!("{}/{}", group, version)
};
let plural = Some(ar.name.clone());
Self {
group,
version,
kind,
api_version,
plural,
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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};
Expand Down
Loading

0 comments on commit 0cadf85

Please sign in to comment.