diff --git a/crates/stackable-versioned-macros/src/attrs/container.rs b/crates/stackable-versioned-macros/src/attrs/container.rs index d902dd1cd..263d15038 100644 --- a/crates/stackable-versioned-macros/src/attrs/container.rs +++ b/crates/stackable-versioned-macros/src/attrs/container.rs @@ -50,6 +50,7 @@ pub struct ContainerSkipArguments { /// cluster scoped resource. /// - `crates`: Override specific crates. /// - `status`: Set the specified struct as the status subresource. +/// - `scale`: Configure the scale subresource for horizontal pod autoscaling integration. /// - `shortname`: Set a shortname for the CR object. This can be specified multiple /// times. /// - `skip`: Controls skipping parts of the generation. @@ -64,7 +65,7 @@ pub struct StructCrdArguments { pub status: Option, // derive // schema - // scale + pub scale: Option, // printcolumn #[darling(multiple, rename = "shortname")] pub shortnames: Vec, @@ -74,3 +75,21 @@ pub struct StructCrdArguments { // annotation // label } + +/// Scale subresource configuration for a CRD. +/// +/// Mirrors the fields of [`k8s_openapi::CustomResourceSubresourceScale`][1] and what is present in +/// `kube_derive`. +/// +/// [1]: k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceSubresourceScale +// +// TODO (@Techassi): This should eventually get replaced by directly using what `kube_derive` offers, +// but that requires an upstream restructure I'm planning to do soon(ish). +#[derive(Clone, Debug, FromMeta)] +pub struct Scale { + pub spec_replicas_path: String, + pub status_replicas_path: String, + + #[darling(default)] + pub label_selector_path: Option, +} diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs index 021f523fa..1334d863d 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs @@ -5,7 +5,7 @@ use quote::quote; use syn::{Generics, ItemStruct}; use crate::{ - attrs::container::{ContainerAttributes, StructCrdArguments}, + attrs::container::{ContainerAttributes, Scale, StructCrdArguments}, codegen::{ Direction, VersionContext, VersionDefinition, changes::Neighbors, @@ -272,6 +272,28 @@ impl Struct { _ => None, }; + let scale = spec_gen_ctx + .kubernetes_arguments + .scale + .as_ref() + .map(|scale| { + let Scale { + spec_replicas_path, + status_replicas_path, + label_selector_path, + } = scale; + + let label_selector_path = label_selector_path + .as_ref() + .map(|p| quote! { , label_selector_path = #p }); + + quote! { , scale( + spec_replicas_path = #spec_replicas_path, + status_replicas_path = #status_replicas_path + #label_selector_path + )} + }); + let shortnames: TokenStream = spec_gen_ctx .kubernetes_arguments .shortnames @@ -286,7 +308,7 @@ impl Struct { // These must be comma separated (except the last) as they always exist: group = #group, version = #version, kind = #kind // These fields are optional, and therefore the token stream must prefix each with a comma: - #singular #plural #namespaced #crates #status #shortnames + #singular #plural #namespaced #crates #status #scale #shortnames )] }) } diff --git a/crates/stackable-versioned-macros/tests/inputs/pass/scale.rs b/crates/stackable-versioned-macros/tests/inputs/pass/scale.rs new file mode 100644 index 000000000..7a0969738 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/inputs/pass/scale.rs @@ -0,0 +1,28 @@ +use stackable_versioned::versioned; +// --- +#[versioned(version(name = "v1alpha1"))] +// --- +pub(crate) mod versioned { + #[versioned(crd( + group = "stackable.tech", + scale( + spec_replicas_path = ".spec.replicas", + status_replicas_path = ".status.replicas", + label_selector_path = ".status.selector" + ) + ))] + #[derive( + Clone, + Debug, + serde::Deserialize, + serde::Serialize, + schemars::JsonSchema, + kube::CustomResource, + )] + struct FooSpec { + bar: usize, + baz: bool, + } +} +// --- +fn main() {} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@scale.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@scale.rs.snap new file mode 100644 index 000000000..e043405e2 --- /dev/null +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshots__pass@scale.rs.snap @@ -0,0 +1,221 @@ +--- +source: crates/stackable-versioned-macros/src/lib.rs +expression: formatted +input_file: crates/stackable-versioned-macros/tests/inputs/pass/scale.rs +--- +#[automatically_derived] +pub(crate) mod v1alpha1 { + use super::*; + #[derive( + Clone, + Debug, + serde::Deserialize, + serde::Serialize, + schemars::JsonSchema, + kube::CustomResource, + )] + #[kube( + group = "stackable.tech", + version = "v1alpha1", + kind = "Foo", + scale( + spec_replicas_path = ".spec.replicas", + status_replicas_path = ".status.replicas", + label_selector_path = ".status.selector" + ) + )] + pub struct FooSpec { + pub bar: usize, + pub baz: bool, + } +} +#[automatically_derived] +#[derive(::core::fmt::Debug)] +pub(crate) enum Foo { + V1Alpha1(v1alpha1::Foo), +} +#[automatically_derived] +impl Foo { + /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. + pub fn merged_crd( + stored_apiversion: FooVersion, + ) -> ::std::result::Result< + ::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, + ::kube::core::crd::MergeError, + > { + ::kube::core::crd::merge_crds( + vec![< v1alpha1::Foo as ::kube::core::CustomResourceExt > ::crd()], + stored_apiversion.as_version_str(), + ) + } + ///Tries to convert a list of objects of kind [`Foo`] to the desired API version + ///specified in the [`ConversionReview`][cr]. + /// + ///The returned [`ConversionReview`][cr] either indicates a success or a failure, which + ///is handed back to the Kubernetes API server. + /// + ///[cr]: ::kube::core::conversion::ConversionReview + pub fn try_convert( + review: ::kube::core::conversion::ConversionReview, + ) -> ::kube::core::conversion::ConversionReview { + let request = match ::kube::core::conversion::ConversionRequest::from_review( + review, + ) { + ::std::result::Result::Ok(request) => request, + ::std::result::Result::Err(err) => { + return ::kube::core::conversion::ConversionResponse::invalid(::kube::core::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: err.to_string(), + metadata: None, + reason: err.to_string(), + details: None, + code: 400, + }) + .into_review(); + } + }; + let response = match Self::convert_objects( + request.objects, + &request.desired_api_version, + ) { + ::std::result::Result::Ok(converted_objects) => { + ::kube::core::conversion::ConversionResponse { + result: ::kube::core::Status::success(), + types: request.types, + uid: request.uid, + converted_objects, + } + } + ::std::result::Result::Err(err) => { + let code = err.http_status_code(); + let message = err.join_errors(); + ::kube::core::conversion::ConversionResponse { + result: ::kube::core::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: message.clone(), + metadata: None, + reason: message, + details: None, + code, + }, + types: request.types, + uid: request.uid, + converted_objects: vec![], + } + } + }; + response.into_review() + } + fn convert_objects( + objects: ::std::vec::Vec<::serde_json::Value>, + desired_api_version: &str, + ) -> ::std::result::Result< + ::std::vec::Vec<::serde_json::Value>, + ::stackable_versioned::ConvertObjectError, + > { + let desired_api_version = FooVersion::from_api_version(desired_api_version) + .map_err(|source| ::stackable_versioned::ConvertObjectError::ParseDesiredApiVersion { + source, + })?; + let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); + for object in objects { + let current_object = Self::from_json_object(object.clone()) + .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { + source, + })?; + match (current_object, desired_api_version) { + _ => converted_objects.push(object), + } + } + ::std::result::Result::Ok(converted_objects) + } + fn from_json_object( + object_value: ::serde_json::Value, + ) -> ::std::result::Result { + let kind = object_value + .get("kind") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { + field: "kind".to_owned(), + })? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "kind".to_owned(), + })?; + if kind != "Foo" { + return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { + kind: kind.to_owned(), + expected: "Foo".to_owned(), + }); + } + let api_version = object_value + .get("apiVersion") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent { + field: "apiVersion".to_owned(), + })? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "apiVersion".to_owned(), + })?; + let object = match api_version { + "stackable.tech/v1alpha1" => { + let object = ::serde_json::from_value(object_value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Alpha1(object) + } + unknown_api_version => { + return ::std::result::Result::Err(::stackable_versioned::ParseObjectError::UnknownApiVersion { + api_version: unknown_api_version.to_owned(), + }); + } + }; + ::std::result::Result::Ok(object) + } + fn into_json_value( + self, + ) -> ::std::result::Result<::serde_json::Value, ::serde_json::Error> { + match self { + Self::V1Alpha1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + } + } +} +#[automatically_derived] +#[derive(::core::marker::Copy, ::core::clone::Clone, ::core::fmt::Debug)] +pub(crate) enum FooVersion { + V1Alpha1, +} +#[automatically_derived] +impl ::core::fmt::Display for FooVersion { + fn fmt( + &self, + f: &mut ::core::fmt::Formatter<'_>, + ) -> ::std::result::Result<(), ::std::fmt::Error> { + f.write_str(self.as_version_str()) + } +} +#[automatically_derived] +impl FooVersion { + pub fn as_version_str(&self) -> &str { + match self { + FooVersion::V1Alpha1 => "v1alpha1", + } + } + pub fn as_api_version_str(&self) -> &str { + match self { + FooVersion::V1Alpha1 => "stackable.tech/v1alpha1", + } + } + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "stackable.tech/v1alpha1" => Ok(FooVersion::V1Alpha1), + _ => { + Err(::stackable_versioned::UnknownDesiredApiVersionError { + api_version: api_version.to_owned(), + }) + } + } + } +} diff --git a/crates/stackable-versioned-macros/tests/trybuild.rs b/crates/stackable-versioned-macros/tests/trybuild.rs index f2a215bd7..5b584ee31 100644 --- a/crates/stackable-versioned-macros/tests/trybuild.rs +++ b/crates/stackable-versioned-macros/tests/trybuild.rs @@ -30,6 +30,7 @@ mod inputs { // mod module_preserve; // mod renamed_field; // mod renamed_kind; + // mod scale; // mod shortnames; // mod submodule; } diff --git a/crates/stackable-versioned/CHANGELOG.md b/crates/stackable-versioned/CHANGELOG.md index d388a4d7d..3f6eb4d01 100644 --- a/crates/stackable-versioned/CHANGELOG.md +++ b/crates/stackable-versioned/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Add support to provide `#[versioned(crd(scale(...)))]` argument to enable the `/scale` subresource ([#1185]). + +[#1185]: https://github.com/stackabletech/operator-rs/pull/1185 + ## [0.8.3] - 2025-10-23 ### Fixed