Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
5 changes: 4 additions & 1 deletion examples/crd_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ use kube::{
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, Validate, JsonSchema)]
#[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)]
#[kube(status = "FooStatus")]
#[kube(scale = r#"{"specReplicasPath":".spec.replicas", "statusReplicasPath":".status.replicas"}"#)]
#[kube(scale(
spec_replicas_path = ".spec.replicas",
status_replicas_path = ".status.replicas"
))]
#[kube(printcolumn = r#"{"name":"Team", "jsonPath": ".spec.metadata.team", "type": "string"}"#)]
pub struct FooSpec {
#[schemars(length(min = 3))]
Expand Down
5 changes: 4 additions & 1 deletion examples/crd_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ use serde::{Deserialize, Serialize};
derive = "PartialEq",
derive = "Default",
shortname = "f",
scale = r#"{"specReplicasPath":".spec.replicas", "statusReplicasPath":".status.replicas"}"#,
scale(
spec_replicas_path = ".spec.replicas",
status_replicas_path = ".status.replicas"
),
printcolumn = r#"{"name":"Spec", "type":"string", "description":"name of foo", "jsonPath":".spec.name"}"#,
selectable = "spec.name"
)]
Expand Down
1 change: 1 addition & 0 deletions kube-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ workspace = true
proc-macro2.workspace = true
quote.workspace = true
syn = { workspace = true, features = ["extra-traits"] }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
darling.workspace = true

Expand Down
125 changes: 118 additions & 7 deletions kube-derive/src/custom_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use darling::{FromDeriveInput, FromMeta};
use proc_macro2::{Ident, Literal, Span, TokenStream};
use quote::{ToTokens, TokenStreamExt as _};
use serde::Deserialize;
use syn::{parse_quote, Data, DeriveInput, Expr, Path, Visibility};

/// Values we can parse from #[kube(attrs)]
Expand Down Expand Up @@ -33,7 +34,12 @@ struct KubeAttrs {
printcolums: Vec<String>,
#[darling(multiple)]
selectable: Vec<String>,
scale: Option<String>,

/// Customize the scale subresource, see [Kubernetes docs][1].
///
/// [1]: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#scale-subresource
scale: Option<Scale>,
Comment thread
clux marked this conversation as resolved.

#[darling(default)]
crates: Crates,
#[darling(multiple, rename = "annotation")]
Expand Down Expand Up @@ -192,6 +198,109 @@ impl FromMeta for SchemaMode {
}
}

/// This struct mirrors the fields of `k8s_openapi::CustomResourceSubresourceScale` to support
/// parsing from the `#[kube]` attribute.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Scale {
pub(crate) label_selector_path: Option<String>,
Comment thread
clux marked this conversation as resolved.
pub(crate) spec_replicas_path: String,
pub(crate) status_replicas_path: String,
}

// This custom FromMeta implementation is needed for two reasons:
//
// - To enable backwards-compatibility. Up to version 0.97.0 it was only possible to set scale
// subresource values as a JSON string.
Comment thread
clux marked this conversation as resolved.
// - To be able to declare the scale sub-resource as a list of typed fields. The from_list impl uses
// the derived implementation as inspiration.
impl FromMeta for Scale {
/// This is implemented for backwards-compatibility. It allows that the scale subresource can
/// be deserialized from a JSON string.
fn from_string(value: &str) -> darling::Result<Self> {
serde_json::from_str(value).map_err(darling::Error::custom)
Comment thread
Techassi marked this conversation as resolved.
}

fn from_list(items: &[darling::ast::NestedMeta]) -> darling::Result<Self> {
let mut errors = darling::Error::accumulator();

let mut label_selector_path: (bool, Option<Option<String>>) = (false, None);
let mut spec_replicas_path: (bool, Option<String>) = (false, None);
let mut status_replicas_path: (bool, Option<String>) = (false, None);

for item in items {
match item {
darling::ast::NestedMeta::Meta(meta) => {
let name = darling::util::path_to_string(meta.path());

match name.as_str() {
"label_selector_path" => {
let path = errors.handle(darling::FromMeta::from_meta(meta));
label_selector_path = (true, Some(path))
}
"spec_replicas_path" => {
let path = errors.handle(darling::FromMeta::from_meta(meta));
spec_replicas_path = (true, path)
}
"status_replicas_path" => {
let path = errors.handle(darling::FromMeta::from_meta(meta));
status_replicas_path = (true, path)
}
other => return Err(darling::Error::unknown_field(other)),
}
}
darling::ast::NestedMeta::Lit(lit) => {
errors.push(darling::Error::unsupported_format("literal").with_span(&lit.span()))
}
}
}

if !label_selector_path.0 {
match <Option<String> as darling::FromMeta>::from_none() {
Some(fallback) => label_selector_path.1 = Some(fallback),
None => errors.push(darling::Error::missing_field("spec_replicas_path")),
}
}

if !spec_replicas_path.0 && spec_replicas_path.1.is_none() {
errors.push(darling::Error::missing_field("spec_replicas_path"));
}

if !status_replicas_path.0 && status_replicas_path.1.is_none() {
errors.push(darling::Error::missing_field("status_replicas_path"));
}

errors.finish_with(Self {
label_selector_path: label_selector_path.1.unwrap(),
spec_replicas_path: spec_replicas_path.1.unwrap(),
status_replicas_path: status_replicas_path.1.unwrap(),
})
}
}

impl Scale {
fn to_tokens(&self, k8s_openapi: &Path) -> TokenStream {
let apiext = quote! {
#k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1
};

let label_selector_path = self
.label_selector_path
.as_ref()
.map_or_else(|| quote! { None }, |p| quote! { #p.into() });
let spec_replicas_path = &self.spec_replicas_path;
let status_replicas_path = &self.status_replicas_path;

quote! {
#apiext::CustomResourceSubresourceScale {
label_selector_path: #label_selector_path,
spec_replicas_path: #spec_replicas_path.into(),
status_replicas_path: #status_replicas_path.into()
}
}
}
}

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(),
Expand Down Expand Up @@ -452,7 +561,13 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea
.map(|s| format!(r#"{{ "jsonPath": "{s}" }}"#))
.collect();
let fields = format!("[ {} ]", fields.join(","));
let scale_code = if let Some(s) = scale { s } else { "".to_string() };
let scale = scale.map_or_else(
|| quote! { None },
|s| {
let scale = s.to_tokens(&k8s_openapi);
quote! { Some(#scale) }
},
);

// Ensure it generates for the correct CRD version (only v1 supported now)
let apiext = quote! {
Expand Down Expand Up @@ -564,11 +679,7 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea
#k8s_openapi::k8s_if_ge_1_30! {
let fields : Vec<#apiext::SelectableField> = #serde_json::from_str(#fields).expect("valid selectableField column json");
}
let scale: Option<#apiext::CustomResourceSubresourceScale> = if #scale_code.is_empty() {
None
} else {
#serde_json::from_str(#scale_code).expect("valid scale subresource json")
};
let scale: Option<#apiext::CustomResourceSubresourceScale> = #scale;
let categories: Vec<String> = #serde_json::from_str(#categories_json).expect("valid categories");
let shorts : Vec<String> = #serde_json::from_str(#short_json).expect("valid shortnames");
let subres = if #has_status {
Expand Down
14 changes: 13 additions & 1 deletion kube-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,21 @@ mod resource;
/// NOTE: `CustomResourceDefinition`s require a schema. If `schema = "disabled"` then
/// `Self::crd()` will not be installable into the cluster as-is.
///
/// ## `#[kube(scale = r#"json"#)]`
/// ## `#[kube(scale(...))]`
///
/// Allow customizing the scale struct for the [scale subresource](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#subresources).
///
/// ```ignore
/// #[kube(scale(
/// specReplicasPath = ".spec.replicas",
/// statusReplicaPath = ".status.replicas",
/// labelSelectorPath = ".spec.labelSelector"
/// ))]
/// ```
///
/// The deprecated way of customizing the scale subresource using a raw JSON string is still
/// support for backwards-compatibility.
///
/// ## `#[kube(printcolumn = r#"json"#)]`
/// Allows adding straight json to [printcolumns](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#additional-printer-columns).
///
Expand Down
2 changes: 1 addition & 1 deletion kube-runtime/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ impl Recorder {
deprecated_source: None,
event_time: Some(MicroTime(now)),
regarding: Some(reference.clone()),
note: ev.note.clone().map(Into::into),
note: ev.note.clone(),
metadata: ObjectMeta {
namespace: reference.namespace.clone(),
name: Some(format!(
Expand Down
5 changes: 4 additions & 1 deletion kube/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,10 @@ mod test {
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
#[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)]
#[kube(status = "FooStatus")]
#[kube(scale = r#"{"specReplicasPath":".spec.replicas", "statusReplicasPath":".status.replicas"}"#)]
#[kube(scale(
spec_replicas_path = ".spec.replicas",
status_replicas_path = ".status.replicas"
))]
#[kube(crates(kube_core = "crate::core"))] // for dev-dep test structure
pub struct FooSpec {
name: String,
Expand Down