From 9537789d570cbc7aa9511977f5c3f8a5e7bf02f6 Mon Sep 17 00:00:00 2001 From: Techassi Date: Mon, 2 Dec 2024 16:29:45 +0100 Subject: [PATCH 1/8] feat: Add typed scale argument to derive macro This allows cutomizing the scale subresource by providing key-value items instead of a raw JSON string. For backwards-compatibility, it is still supported to provide a JSON string. However, all examples and tests were converted to the new format. Signed-off-by: Techassi --- examples/crd_api.rs | 5 +- examples/crd_derive.rs | 5 +- kube-derive/Cargo.toml | 1 + kube-derive/src/custom_resource.rs | 123 +++++++++++++++++++++++++++-- kube/src/lib.rs | 5 +- 5 files changed, 129 insertions(+), 10 deletions(-) diff --git a/examples/crd_api.rs b/examples/crd_api.rs index dfbd52e7e..d0123b28b 100644 --- a/examples/crd_api.rs +++ b/examples/crd_api.rs @@ -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))] diff --git a/examples/crd_derive.rs b/examples/crd_derive.rs index 4aace0193..0ec836aeb 100644 --- a/examples/crd_derive.rs +++ b/examples/crd_derive.rs @@ -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" )] diff --git a/kube-derive/Cargo.toml b/kube-derive/Cargo.toml index 0b89ea2f0..a758602b1 100644 --- a/kube-derive/Cargo.toml +++ b/kube-derive/Cargo.toml @@ -19,6 +19,7 @@ proc-macro2.workspace = true quote.workspace = true syn = { workspace = true, features = ["extra-traits"] } serde_json.workspace = true +k8s-openapi = { workspace = true, features = ["latest"] } darling.workspace = true [lib] diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index 055664f31..b0c8f3f13 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -2,6 +2,7 @@ #![allow(clippy::manual_unwrap_or_default)] use darling::{FromDeriveInput, FromMeta}; +use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceSubresourceScale; use proc_macro2::{Ident, Literal, Span, TokenStream}; use quote::{ToTokens, TokenStreamExt}; use syn::{parse_quote, Data, DeriveInput, Path, Visibility}; @@ -34,7 +35,12 @@ struct KubeAttrs { printcolums: Vec, #[darling(multiple)] selectable: Vec, - scale: Option, + + /// 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, + #[darling(default)] crates: Crates, #[darling(multiple, rename = "annotation")] @@ -185,6 +191,107 @@ impl FromMeta for SchemaMode { } } +/// A new-type wrapper around [`CustomResourceSubresourceScale`] to support parsing from the +/// `#[kube]` attribute. +#[derive(Debug)] +struct Scale(CustomResourceSubresourceScale); + +// 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. +// - k8s_openapi types don't support being parsed directly from attributes using darling. This +// would require an upstream change, which is highly unlikely to occur. 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 { + let scale = serde_json::from_str(value).map_err(|err| darling::Error::custom(err))?; + Ok(Self(scale)) + } + + fn from_list(items: &[darling::ast::NestedMeta]) -> darling::Result { + let mut errors = darling::Error::accumulator(); + + let mut label_selector_path: (bool, Option>) = (false, None); + let mut spec_replicas_path: (bool, Option) = (false, None); + let mut status_replicas_path: (bool, Option) = (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 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(CustomResourceSubresourceScale { + 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 + .0 + .label_selector_path + .as_ref() + .map_or_else(|| quote! { None }, |p| quote! { #p.into() }); + let spec_replicas_path = &self.0.spec_replicas_path; + let status_replicas_path = &self.0.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(), @@ -439,7 +546,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! { @@ -551,11 +664,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 = #serde_json::from_str(#categories_json).expect("valid categories"); let shorts : Vec = #serde_json::from_str(#short_json).expect("valid shortnames"); let subres = if #has_status { diff --git a/kube/src/lib.rs b/kube/src/lib.rs index e7be35690..0684c4fb9 100644 --- a/kube/src/lib.rs +++ b/kube/src/lib.rs @@ -223,7 +223,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, From d93d70845d72be81a50dbba7bac21fc0c9a5c6be Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 23 Jan 2025 16:34:53 +0100 Subject: [PATCH 2/8] refactor: Remove k8s_openapi dependency Signed-off-by: Techassi --- kube-derive/Cargo.toml | 2 +- kube-derive/src/custom_resource.rs | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/kube-derive/Cargo.toml b/kube-derive/Cargo.toml index a758602b1..c50dae035 100644 --- a/kube-derive/Cargo.toml +++ b/kube-derive/Cargo.toml @@ -18,8 +18,8 @@ workspace = true proc-macro2.workspace = true quote.workspace = true syn = { workspace = true, features = ["extra-traits"] } +serde.workspace = true serde_json.workspace = true -k8s-openapi = { workspace = true, features = ["latest"] } darling.workspace = true [lib] diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index b0c8f3f13..eb24e1161 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -2,9 +2,9 @@ #![allow(clippy::manual_unwrap_or_default)] use darling::{FromDeriveInput, FromMeta}; -use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceSubresourceScale; use proc_macro2::{Ident, Literal, Span, TokenStream}; use quote::{ToTokens, TokenStreamExt}; +use serde::Deserialize; use syn::{parse_quote, Data, DeriveInput, Path, Visibility}; /// Values we can parse from #[kube(attrs)] @@ -191,10 +191,15 @@ impl FromMeta for SchemaMode { } } -/// A new-type wrapper around [`CustomResourceSubresourceScale`] to support parsing from the -/// `#[kube]` attribute. -#[derive(Debug)] -struct Scale(CustomResourceSubresourceScale); +/// 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, + pub(crate) spec_replicas_path: String, + pub(crate) status_replicas_path: String, +} // This custom FromMeta implementation is needed for two reasons: // @@ -207,8 +212,7 @@ 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 { - let scale = serde_json::from_str(value).map_err(|err| darling::Error::custom(err))?; - Ok(Self(scale)) + serde_json::from_str(value).map_err(|err| darling::Error::custom(err)) } fn from_list(items: &[darling::ast::NestedMeta]) -> darling::Result { @@ -260,11 +264,11 @@ impl FromMeta for Scale { errors.push(darling::Error::missing_field("status_replicas_path")); } - errors.finish_with(Self(CustomResourceSubresourceScale { + 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(), - })) + }) } } @@ -275,12 +279,11 @@ impl Scale { }; let label_selector_path = self - .0 .label_selector_path .as_ref() .map_or_else(|| quote! { None }, |p| quote! { #p.into() }); - let spec_replicas_path = &self.0.spec_replicas_path; - let status_replicas_path = &self.0.status_replicas_path; + let spec_replicas_path = &self.spec_replicas_path; + let status_replicas_path = &self.status_replicas_path; quote! { #apiext::CustomResourceSubresourceScale { From c8a8a3b7cf4c5962563147d02ab07a084b87b824 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 23 Jan 2025 16:45:44 +0100 Subject: [PATCH 3/8] chore: Adjust doc comment, fix clippy lint Signed-off-by: Techassi --- kube-derive/src/custom_resource.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index e8b78cf2e..12bb9888d 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -212,14 +212,13 @@ struct Scale { // // - To enable backwards-compatibility. Up to version 0.97.0 it was only possible to set scale // subresource values as a JSON string. -// - k8s_openapi types don't support being parsed directly from attributes using darling. This -// would require an upstream change, which is highly unlikely to occur. The from_list impl uses +// - 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 { - serde_json::from_str(value).map_err(|err| darling::Error::custom(err)) + serde_json::from_str(value).map_err(darling::Error::custom) } fn from_list(items: &[darling::ast::NestedMeta]) -> darling::Result { From 7ceb796c9e916f7f2f388cfebde42daa4127ecb7 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 23 Jan 2025 16:46:03 +0100 Subject: [PATCH 4/8] chore: Fix clippy lint in kube-runtime Signed-off-by: Techassi --- kube-runtime/src/events.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kube-runtime/src/events.rs b/kube-runtime/src/events.rs index 5b811977b..d62873315 100644 --- a/kube-runtime/src/events.rs +++ b/kube-runtime/src/events.rs @@ -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!( From a56835e1a80578c0c3d784cab913ed0b21ebc533 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 23 Jan 2025 17:03:07 +0100 Subject: [PATCH 5/8] docs: Adjust doc comment Signed-off-by: Techassi --- kube-derive/src/lib.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/kube-derive/src/lib.rs b/kube-derive/src/lib.rs index 83e008caa..a03217d76 100644 --- a/kube-derive/src/lib.rs +++ b/kube-derive/src/lib.rs @@ -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). /// +/// ```no_run +/// #[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). /// From d513d16da0862274e9dc0246b0b47317654fdbc4 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 23 Jan 2025 17:03:24 +0100 Subject: [PATCH 6/8] chore: Use serde derive feature to enable derive macro Signed-off-by: Techassi --- kube-derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kube-derive/Cargo.toml b/kube-derive/Cargo.toml index bec4990f6..a1821f640 100644 --- a/kube-derive/Cargo.toml +++ b/kube-derive/Cargo.toml @@ -18,7 +18,7 @@ workspace = true proc-macro2.workspace = true quote.workspace = true syn = { workspace = true, features = ["extra-traits"] } -serde.workspace = true +serde = { workspace = true, features = ["derive"] } serde_json.workspace = true darling.workspace = true From 7a8611732647612bb928e706f66a6595e0d962fa Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 23 Jan 2025 17:11:44 +0100 Subject: [PATCH 7/8] chore: Use ignore instead of no_run Signed-off-by: Techassi --- kube-derive/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kube-derive/src/lib.rs b/kube-derive/src/lib.rs index a03217d76..45db03408 100644 --- a/kube-derive/src/lib.rs +++ b/kube-derive/src/lib.rs @@ -133,7 +133,7 @@ mod resource; /// /// Allow customizing the scale struct for the [scale subresource](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#subresources). /// -/// ```no_run +/// ```ignore /// #[kube(scale( /// specReplicasPath = ".spec.replicas", /// statusReplicaPath = ".status.replicas", From 696d38945cefc87343dc56d441f869f627382158 Mon Sep 17 00:00:00 2001 From: Techassi Date: Sat, 25 Jan 2025 13:48:24 +0100 Subject: [PATCH 8/8] test: Add schema test, fix FromMeta implementation Adding this test proved to be very valuable because the FromMeta implemenetation had a few errors and resulted in different panic messages coming from the derive macro. I also added a small note to the #[kube(scale(...))] section stating that the scale subresource can only be used when the status subresource is used as well. I plan to further improve the validation in a future pull request. Signed-off-by: Techassi --- kube-derive/src/custom_resource.rs | 47 ++++++++++++++++++---------- kube-derive/src/lib.rs | 2 ++ kube-derive/tests/crd_schema_test.rs | 41 +++++++++++++++++++++++- 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index 12bb9888d..8c760864e 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -235,18 +235,36 @@ impl FromMeta for Scale { match name.as_str() { "label_selector_path" => { - let path = errors.handle(darling::FromMeta::from_meta(meta)); - label_selector_path = (true, Some(path)) + if !label_selector_path.0 { + let path = errors.handle(darling::FromMeta::from_meta(meta)); + label_selector_path = (true, Some(path)) + } else { + errors.push( + darling::Error::duplicate_field("label_selector_path").with_span(&meta), + ); + } } "spec_replicas_path" => { - let path = errors.handle(darling::FromMeta::from_meta(meta)); - spec_replicas_path = (true, path) + if !spec_replicas_path.0 { + let path = errors.handle(darling::FromMeta::from_meta(meta)); + spec_replicas_path = (true, path) + } else { + errors.push( + darling::Error::duplicate_field("spec_replicas_path").with_span(&meta), + ); + } } "status_replicas_path" => { - let path = errors.handle(darling::FromMeta::from_meta(meta)); - status_replicas_path = (true, path) + if !status_replicas_path.0 { + let path = errors.handle(darling::FromMeta::from_meta(meta)); + status_replicas_path = (true, path) + } else { + errors.push( + darling::Error::duplicate_field("status_replicas_path").with_span(&meta), + ); + } } - other => return Err(darling::Error::unknown_field(other)), + other => errors.push(darling::Error::unknown_field(other)), } } darling::ast::NestedMeta::Lit(lit) => { @@ -255,13 +273,6 @@ impl FromMeta for Scale { } } - if !label_selector_path.0 { - match 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")); } @@ -270,8 +281,10 @@ impl FromMeta for Scale { errors.push(darling::Error::missing_field("status_replicas_path")); } - errors.finish_with(Self { - label_selector_path: label_selector_path.1.unwrap(), + errors.finish()?; + + Ok(Self { + label_selector_path: label_selector_path.1.unwrap_or_default(), spec_replicas_path: spec_replicas_path.1.unwrap(), status_replicas_path: status_replicas_path.1.unwrap(), }) @@ -287,7 +300,7 @@ impl Scale { let label_selector_path = self .label_selector_path .as_ref() - .map_or_else(|| quote! { None }, |p| quote! { #p.into() }); + .map_or_else(|| quote! { None }, |p| quote! { Some(#p.into()) }); let spec_replicas_path = &self.spec_replicas_path; let status_replicas_path = &self.status_replicas_path; diff --git a/kube-derive/src/lib.rs b/kube-derive/src/lib.rs index 45db03408..f0049282c 100644 --- a/kube-derive/src/lib.rs +++ b/kube-derive/src/lib.rs @@ -132,6 +132,8 @@ mod resource; /// ## `#[kube(scale(...))]` /// /// Allow customizing the scale struct for the [scale subresource](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#subresources). +/// It should be noted, that the status subresource must also be enabled to use the scale subresource. This is because +/// the `statusReplicasPath` only accepts JSONPaths under `.status`. /// /// ```ignore /// #[kube(scale( diff --git a/kube-derive/tests/crd_schema_test.rs b/kube-derive/tests/crd_schema_test.rs index 8e8c5cf07..e7c65bfce 100644 --- a/kube-derive/tests/crd_schema_test.rs +++ b/kube-derive/tests/crd_schema_test.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] #![recursion_limit = "256"] use assert_json_diff::assert_json_eq; @@ -29,6 +30,12 @@ use std::collections::{HashMap, HashSet}; label("clux.dev", "cluxingv1"), label("clux.dev/persistence", "disabled"), rule = Rule::new("self.metadata.name == 'singleton'"), + status = "Status", + scale( + spec_replicas_path = ".spec.replicas", + status_replicas_path = ".status.replicas", + label_selector_path = ".status.labelSelector" + ), )] #[cel_validate(rule = Rule::new("has(self.nonNullable)"))] #[serde(rename_all = "camelCase")] @@ -62,6 +69,13 @@ struct FooSpec { set: HashSet, } +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct Status { + replicas: usize, + label_selector: String, +} + fn default_value() -> String { "default_value".into() } @@ -231,6 +245,14 @@ fn test_crd_schema_matches_expected() { }, { "jsonPath": ".spec.nullable" }], + "subresources": { + "status": {}, + "scale": { + "specReplicasPath": ".spec.replicas", + "labelSelectorPath": ".status.labelSelector", + "statusReplicasPath": ".status.replicas" + } + }, "schema": { "openAPIV3Schema": { "description": "Custom resource representing a Foo", @@ -358,6 +380,24 @@ fn test_crd_schema_matches_expected() { "rule": "has(self.nonNullable)", }], "type": "object" + }, + "status": { + "properties": { + "replicas": { + "type": "integer", + "format": "uint", + "minimum": 0.0, + }, + "labelSelector": { + "type": "string" + } + }, + "required": [ + "labelSelector", + "replicas" + ], + "nullable": true, + "type": "object" } }, "required": [ @@ -370,7 +410,6 @@ fn test_crd_schema_matches_expected() { "type": "object" } }, - "subresources": {}, } ] }