Skip to content

Commit

Permalink
Add Resource derive macro (#1565)
Browse files Browse the repository at this point in the history
* Add ResourceInherit derive macro

Allows to generate Resource trait implementation for types which proxy
another type internally.

ConfigMaps and Secrets can be strictly typed on the client side. While
using DeserializeGuard, resources can be listed and watched, skipping
invalid resources.

Signed-off-by: Danil-Grigorev <danil.grigorev@suse.com>

* Rename ResourceInherit to Resource

- Display usage in errorbounded CM watcher

Signed-off-by: Danil-Grigorev <danil.grigorev@suse.com>

* Reverse order resource/inherit

Signed-off-by: Danil-Grigorev <danil.grigorev@suse.com>

* Add cert_check example

Signed-off-by: Danil-Grigorev <danil.grigorev@suse.com>

* Example fmt fixes

Signed-off-by: Danil-Grigorev <danil.grigorev@suse.com>

* Review: comment and cargo fix

Signed-off-by: Danil-Grigorev <danil.grigorev@suse.com>

* Examples readme fix

Signed-off-by: Danil-Grigorev <danil.grigorev@suse.com>

---------

Signed-off-by: Danil-Grigorev <danil.grigorev@suse.com>
Co-authored-by: Eirik A <sszynrae@gmail.com>
  • Loading branch information
Danil-Grigorev and clux committed Sep 9, 2024
1 parent c975335 commit f45ac81
Show file tree
Hide file tree
Showing 8 changed files with 357 additions and 35 deletions.
4 changes: 4 additions & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand All @@ -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`:
Expand Down
112 changes: 112 additions & 0 deletions examples/cert_check.rs
Original file line number Diff line number Diff line change
@@ -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::<Ns>(&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(())
}
39 changes: 4 additions & 35 deletions examples/errorbounded_configmap_watcher.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -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();
Expand Down
47 changes: 47 additions & 0 deletions kube-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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<FooMapSpec>,
/// }
///
/// #[derive(Clone, Debug, Deserialize)]
/// struct FooMapSpec {
/// field: String,
/// }
///
/// let client: Client = todo!();
/// let api: Api<FooMap> = 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()
}
127 changes: 127 additions & 0 deletions kube-derive/src/resource.rs
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading

0 comments on commit f45ac81

Please sign in to comment.