diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 46b656b0d..7b7b14362 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -224,6 +224,10 @@ required-features = ["openssl-tls", "rustls-tls"] name = "custom_client_trace" path = "custom_client_trace.rs" +[[example]] +name = "cert_check" +path = "cert_check.rs" + [[example]] name = "secret_syncer" path = "secret_syncer.rs" diff --git a/examples/README.md b/examples/README.md index 19eb7df7b..e672bd203 100644 --- a/examples/README.md +++ b/examples/README.md @@ -69,6 +69,8 @@ cargo run --example crd_api cargo run --example crd_derive cargo run --example crd_derive_schema cargo run --example crd_derive_no_schema --no-default-features --features=openssl-tls,latest +# collect kube-root configmaps from each namespace, with strictly typed serialization on ca.crt key +cargo run --example cert_check ``` The last one opts out from the default `schema` feature from `kube-derive` (and thus the need for you to derive/impl `JsonSchema`). @@ -93,6 +95,8 @@ cargo run --example multi_watcher cargo run --example node_watcher # watch arbitrary, untyped objects across all namespaces cargo run --example dynamic_watcher +# watch arbitrary, typed config map objects, with error toleration +cargo run --example errorbounded_configmap_watcher ``` The `node_` and `pod_` watcher also allows using [Kubernetes 1.27 Streaming lists](https://kubernetes.io/docs/reference/using-api/api-concepts/#streaming-lists) via `WATCHLIST=1`: diff --git a/examples/cert_check.rs b/examples/cert_check.rs new file mode 100644 index 000000000..0df692293 --- /dev/null +++ b/examples/cert_check.rs @@ -0,0 +1,112 @@ +use std::borrow::Cow; + +use k8s_openapi::{ + api::core::v1::{ConfigMap, Namespace as Ns}, + NamespaceResourceScope, +}; +use kube::{ + api::ObjectMeta, + client::scope::{Cluster, Namespace}, + Client, Resource, ResourceExt, +}; +use serde::{Deserialize, Serialize}; +use tracing::*; + +use thiserror::Error; + +#[derive(Debug, Error)] +enum Error { + #[error("Failed to open client: {0}")] + ClientSetup(#[source] kube::Error), + #[error("Failed to list namespaces: {0}")] + NamespaceList(#[source] kube::Error), + #[error("Failed to get ConfigMap: {0}")] + FetchFailed(#[from] kube::Error), + #[error("Expected certificate key in ConfigMap: {0}")] + MissingKey(#[from] serde_json::Error), +} + +// Variant of ConfigMap that only accepts ConfigMaps with a CA certificate +// to demonstrate manual implementation +#[derive(Serialize, Deserialize, Debug, Clone)] +struct CaConfigMapManual { + metadata: ObjectMeta, + data: CaConfigMapData, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct CaConfigMapData { + #[serde(rename = "ca.crt")] + ca_crt: String, +} + +// Variant of ConfigMap that only accepts ConfigMaps with a CA certificate +// with inherited resource implementation +#[derive(Resource, Serialize, Deserialize, Debug, Clone)] +#[resource(inherit = ConfigMap)] +struct CaConfigMap { + metadata: ObjectMeta, + data: CaConfigMapData, +} + +// Display of a manual implementation +impl Resource for CaConfigMapManual { + type DynamicType = (); + type Scope = NamespaceResourceScope; + + fn kind(&(): &Self::DynamicType) -> Cow<'_, str> { + Cow::Borrowed("ConfigMap") + } + + fn group(&(): &Self::DynamicType) -> Cow<'_, str> { + Cow::Borrowed("") + } + + fn version(&(): &Self::DynamicType) -> Cow<'_, str> { + Cow::Borrowed("v1") + } + + fn plural(&(): &Self::DynamicType) -> Cow<'_, str> { + Cow::Borrowed("configmaps") + } + + fn meta(&self) -> &ObjectMeta { + &self.metadata + } + + fn meta_mut(&mut self) -> &mut ObjectMeta { + &mut self.metadata + } +} + + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing_subscriber::fmt::init(); + + let client = Client::try_default().await.map_err(Error::ClientSetup)?; + let namespaces = client + .list::(&Default::default(), &Cluster) + .await + .map_err(Error::NamespaceList)?; + + for ns in namespaces { + // Equivalent ways to GET using different structs and different Resource impls, with added field validation on top. + let _ca: ConfigMap = client + .get("kube-root-ca.crt", &Namespace::from(ns.name_any())) + .await?; + let _ca: CaConfigMapManual = client + .get("kube-root-ca.crt", &Namespace::from(ns.name_any())) + .await?; + let ca: CaConfigMap = client + .get("kube-root-ca.crt", &Namespace::from(ns.name_any())) + .await?; + info!( + "Found correct root ca config map in {}: {}", + ns.name_any(), + ca.name_any() + ); + } + + Ok(()) +} diff --git a/examples/errorbounded_configmap_watcher.rs b/examples/errorbounded_configmap_watcher.rs index a327087ee..db9e69830 100644 --- a/examples/errorbounded_configmap_watcher.rs +++ b/examples/errorbounded_configmap_watcher.rs @@ -1,9 +1,7 @@ -use std::borrow::Cow; - use futures::prelude::*; -use k8s_openapi::{api::core::v1::Pod, NamespaceResourceScope}; +use k8s_openapi::api::core::v1::ConfigMap; use kube::{ - api::{Api, ObjectMeta, ResourceExt}, + api::{Api, ObjectMeta}, core::DeserializeGuard, runtime::{reflector::ObjectRef, watcher, WatchStreamExt}, Client, Resource, @@ -13,7 +11,8 @@ use tracing::*; // Variant of ConfigMap that only accepts ConfigMaps with a CA certificate // to demonstrate parsing failure -#[derive(Deserialize, Debug, Clone)] +#[derive(Resource, Deserialize, Debug, Clone)] +#[resource(inherit = ConfigMap)] struct CaConfigMap { metadata: ObjectMeta, data: CaConfigMapData, @@ -25,36 +24,6 @@ struct CaConfigMapData { ca_crt: String, } -// Normally you would derive this, but ConfigMap doesn't follow the standard spec/status pattern -impl Resource for CaConfigMap { - type DynamicType = (); - type Scope = NamespaceResourceScope; - - fn kind(&(): &Self::DynamicType) -> Cow<'_, str> { - Cow::Borrowed("ConfigMap") - } - - fn group(&(): &Self::DynamicType) -> Cow<'_, str> { - Cow::Borrowed("") - } - - fn version(&(): &Self::DynamicType) -> Cow<'_, str> { - Cow::Borrowed("v1") - } - - fn plural(&(): &Self::DynamicType) -> Cow<'_, str> { - Cow::Borrowed("configmaps") - } - - fn meta(&self) -> &ObjectMeta { - &self.metadata - } - - fn meta_mut(&mut self) -> &mut ObjectMeta { - &mut self.metadata - } -} - #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); diff --git a/kube-derive/src/lib.rs b/kube-derive/src/lib.rs index a6ea66ab3..0c59066cc 100644 --- a/kube-derive/src/lib.rs +++ b/kube-derive/src/lib.rs @@ -4,6 +4,7 @@ extern crate proc_macro; #[macro_use] extern crate quote; mod custom_resource; +mod resource; /// A custom derive for kubernetes custom resource definitions. /// @@ -308,3 +309,49 @@ mod custom_resource; pub fn derive_custom_resource(input: proc_macro::TokenStream) -> proc_macro::TokenStream { custom_resource::derive(proc_macro2::TokenStream::from(input)).into() } + +/// A custom derive for inheriting Resource impl for the type. +/// +/// This will generate a [`kube::Resource`] trait implementation, which inherits the specified +/// resources trait implementation. +/// +/// Such implementation allows to add strict typing to some typical resources like `Secret` or `ConfigMap`, +/// in cases when implementing CRD is not desirable or it does not fit the use-case. +/// +/// This object can be used with [`kube::Api`]. +/// +/// # Example +/// +/// ```rust,no_run +/// use kube::api::ObjectMeta; +/// use k8s_openapi::api::core::v1::ConfigMap; +/// use kube_derive::Resource; +/// use kube::Client; +/// use kube::Api; +/// use serde::Deserialize; +/// +/// #[derive(Resource, Clone, Debug, Deserialize)] +/// #[resource(inherit = "ConfigMap")] +/// struct FooMap { +/// metadata: ObjectMeta, +/// data: Option, +/// } +/// +/// #[derive(Clone, Debug, Deserialize)] +/// struct FooMapSpec { +/// field: String, +/// } +/// +/// let client: Client = todo!(); +/// let api: Api = Api::default_namespaced(client); +/// let config_map = api.get("with-field"); +/// ``` +/// +/// The example above will generate: +/// ``` +/// // impl kube::Resource for FooMap { .. } +/// ``` +#[proc_macro_derive(Resource, attributes(resource))] +pub fn derive_resource(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + resource::derive(proc_macro2::TokenStream::from(input)).into() +} diff --git a/kube-derive/src/resource.rs b/kube-derive/src/resource.rs new file mode 100644 index 000000000..69b1929a6 --- /dev/null +++ b/kube-derive/src/resource.rs @@ -0,0 +1,127 @@ +// Generated by darling macros, out of our control +#![allow(clippy::manual_unwrap_or_default)] + +use darling::{FromDeriveInput, FromMeta}; +use syn::{parse_quote, Data, DeriveInput, Path}; + +/// Values we can parse from #[kube(attrs)] +#[derive(Debug, FromDeriveInput)] +#[darling(attributes(resource))] +struct InheritAttrs { + inherit: syn::Path, + #[darling(default)] + crates: Crates, +} + +#[derive(Debug, FromMeta)] +struct Crates { + #[darling(default = "Self::default_kube_core")] + kube_core: Path, + #[darling(default = "Self::default_k8s_openapi")] + k8s_openapi: Path, +} + +// Default is required when the subattribute isn't mentioned at all +// Delegate to darling rather than deriving, so that we can piggyback off the `#[darling(default)]` clauses +impl Default for Crates { + fn default() -> Self { + Self::from_list(&[]).unwrap() + } +} + +impl Crates { + fn default_kube_core() -> Path { + parse_quote! { ::kube::core } // by default must work well with people using facade crate + } + + fn default_k8s_openapi() -> Path { + parse_quote! { ::k8s_openapi } + } +} + +pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream { + let derive_input: DeriveInput = match syn::parse2(input) { + Err(err) => return err.to_compile_error(), + Ok(di) => di, + }; + // Limit derive to structs + match derive_input.data { + Data::Struct(_) | Data::Enum(_) => {} + _ => { + return syn::Error::new_spanned(&derive_input.ident, r#"Unions can not #[derive(Resource)]"#) + .to_compile_error() + } + } + let kube_attrs = match InheritAttrs::from_derive_input(&derive_input) { + Err(err) => return err.write_errors(), + Ok(attrs) => attrs, + }; + + let InheritAttrs { + inherit: resource, + crates: Crates { + kube_core, + k8s_openapi, + }, + .. + } = kube_attrs; + + let rootident = derive_input.ident; + + let inherit_resource = quote! { + impl #kube_core::Resource for #rootident { + type DynamicType = <#resource as #kube_core::Resource>::DynamicType; + type Scope = <#resource as #kube_core::Resource>::Scope; + + fn group(_: &<#resource as #kube_core::Resource>::DynamicType) -> std::borrow::Cow<'_, str> { + #resource::group(&Default::default()).into_owned().into() + } + + fn kind(_: &<#resource as #kube_core::Resource>::DynamicType) -> std::borrow::Cow<'_, str> { + #resource::kind(&Default::default()).into_owned().into() + } + + fn version(_: &<#resource as #kube_core::Resource>::DynamicType) -> std::borrow::Cow<'_, str> { + #resource::version(&Default::default()).into_owned().into() + } + + fn api_version(_: &<#resource as #kube_core::Resource>::DynamicType) -> std::borrow::Cow<'_, str> { + #resource::api_version(&Default::default()).into_owned().into() + } + + fn plural(_: &<#resource as #kube_core::Resource>::DynamicType) -> std::borrow::Cow<'_, str> { + #resource::plural(&Default::default()).into_owned().into() + } + + fn meta(&self) -> &#k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { + &self.metadata + } + + fn meta_mut(&mut self) -> &mut #k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { + &mut self.metadata + } + } + }; + + // Concat output + quote! { + #inherit_resource + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_inherit() { + let input = quote! { + #[derive(Resource)] + #[resource(inherit = "ConfigMap")] + struct Foo { metadata: ObjectMeta } + }; + + let input = syn::parse2(input).unwrap(); + InheritAttrs::from_derive_input(&input).unwrap(); + } +} diff --git a/kube-derive/tests/resource.rs b/kube-derive/tests/resource.rs new file mode 100644 index 000000000..e784c38cb --- /dev/null +++ b/kube-derive/tests/resource.rs @@ -0,0 +1,55 @@ +use k8s_openapi::{ + api::core::v1::{ConfigMap, Secret}, + ByteString, +}; +use kube::api::ObjectMeta; +use kube_derive::Resource; + +#[derive(Resource, Default)] +#[resource(inherit = "ConfigMap")] +struct TypedMap { + metadata: ObjectMeta, + data: Option, +} + +#[derive(Default)] +struct TypedData { + field: String, +} + +#[derive(Resource, Default)] +#[resource(inherit = "Secret")] +struct TypedSecret { + metadata: ObjectMeta, + data: Option, +} + +#[derive(Default)] +struct TypedSecretData { + field: ByteString, +} + +#[cfg(test)] +mod tests { + use kube::Resource; + + use crate::{TypedMap, TypedSecret}; + + #[test] + fn test_parse_config_map_default() { + TypedMap::default(); + assert_eq!(TypedMap::kind(&()), "ConfigMap"); + assert_eq!(TypedMap::api_version(&()), "v1"); + assert_eq!(TypedMap::group(&()), ""); + assert_eq!(TypedMap::plural(&()), "configmaps"); + } + + #[test] + fn test_parse_secret_default() { + TypedSecret::default(); + assert_eq!(TypedSecret::kind(&()), "Secret"); + assert_eq!(TypedSecret::api_version(&()), "v1"); + assert_eq!(TypedSecret::group(&()), ""); + assert_eq!(TypedSecret::plural(&()), "secrets"); + } +} diff --git a/kube/src/lib.rs b/kube/src/lib.rs index af8f7490b..73c3fd47e 100644 --- a/kube/src/lib.rs +++ b/kube/src/lib.rs @@ -165,6 +165,10 @@ cfg_error! { #[cfg_attr(docsrs, doc(cfg(feature = "derive")))] pub use kube_derive::CustomResource; +#[cfg(feature = "derive")] +#[cfg_attr(docsrs, doc(cfg(feature = "derive")))] +pub use kube_derive::Resource; + /// Re-exports from `kube-runtime` #[cfg(feature = "runtime")] #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]