Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add plural to Resource and GroupVersionKind #481

Merged
merged 7 commits into from
Mar 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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