diff --git a/.vscode/settings.json b/.vscode/settings.json index 8d9323a24..8c9fdee9f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "rust-analyzer.cargo.features": "all", + "rust-analyzer.imports.granularity.group": "crate", "rust-analyzer.rustfmt.overrideCommand": [ "rustfmt", "+nightly-2025-05-26", diff --git a/Cargo.lock b/Cargo.lock index 9fb3c6a90..72325c8bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1416,6 +1416,12 @@ dependencies = [ "hashbrown 0.15.3", ] +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + [[package]] name = "insta" version = "1.43.1" @@ -1985,6 +1991,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pem" version = "3.0.5" @@ -3053,6 +3065,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "snafu 0.8.5", "stackable-versioned-macros", ] @@ -3062,6 +3075,7 @@ version = "0.7.1" dependencies = [ "convert_case", "darling", + "indoc", "insta", "itertools", "k8s-openapi", @@ -3079,6 +3093,7 @@ dependencies = [ "snafu 0.8.5", "stackable-versioned", "syn 2.0.101", + "tracing", "trybuild", ] @@ -4110,6 +4125,7 @@ name = "xtask" version = "0.0.0" dependencies = [ "clap", + "paste", "serde", "serde_json", "snafu 0.8.5", diff --git a/Cargo.toml b/Cargo.toml index cd584f7ef..58be80063 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,8 @@ educe = { version = "0.6.0", default-features = false, features = ["Clone", "De either = "1.13.0" futures = "0.3.30" futures-util = "0.3.30" -indexmap = "2.5" +indexmap = "2.5.0" +indoc = "2.0.6" insta = { version= "1.40", features = ["glob"] } hyper = { version = "1.4.1", features = ["full"] } hyper-util = "0.1.8" @@ -41,6 +42,7 @@ opentelemetry-appender-tracing = "0.29.1" opentelemetry-otlp = "0.29.0" # opentelemetry-semantic-conventions = "0.28.0" p256 = { version = "0.13.2", features = ["ecdsa"] } +paste = "1.0.15" pin-project = "1.1.5" prettyplease = "0.2.22" proc-macro2 = "1.0.86" diff --git a/crates/k8s-version/src/api_version/darling.rs b/crates/k8s-version/src/api_version/darling.rs new file mode 100644 index 000000000..e81ad2e88 --- /dev/null +++ b/crates/k8s-version/src/api_version/darling.rs @@ -0,0 +1,35 @@ +use std::str::FromStr; + +use darling::FromMeta; + +use crate::ApiVersion; + +impl FromMeta for ApiVersion { + fn from_string(value: &str) -> darling::Result { + Self::from_str(value).map_err(darling::Error::custom) + } +} + +#[cfg(test)] +mod test { + use quote::quote; + use rstest::rstest; + + use super::*; + use crate::{Level, Version}; + + fn parse_meta(tokens: proc_macro2::TokenStream) -> ::std::result::Result { + let attribute: syn::Attribute = syn::parse_quote!(#[#tokens]); + Ok(attribute.meta) + } + + #[rstest] + #[case(quote!(ignore = "extensions/v1beta1"), ApiVersion { group: Some("extensions".parse().unwrap()), version: Version { major: 1, level: Some(Level::Beta(1)) } })] + #[case(quote!(ignore = "v1beta1"), ApiVersion { group: None, version: Version { major: 1, level: Some(Level::Beta(1)) } })] + #[case(quote!(ignore = "v1"), ApiVersion { group: None, version: Version { major: 1, level: None } })] + fn from_meta(#[case] input: proc_macro2::TokenStream, #[case] expected: ApiVersion) { + let meta = parse_meta(input).expect("valid attribute tokens"); + let api_version = ApiVersion::from_meta(&meta).expect("version must parse from attribute"); + assert_eq!(api_version, expected); + } +} diff --git a/crates/k8s-version/src/api_version/mod.rs b/crates/k8s-version/src/api_version/mod.rs index 5f046a869..bdd72193b 100644 --- a/crates/k8s-version/src/api_version/mod.rs +++ b/crates/k8s-version/src/api_version/mod.rs @@ -1,7 +1,5 @@ use std::{cmp::Ordering, fmt::Display, str::FromStr}; -#[cfg(feature = "darling")] -use darling::FromMeta; use snafu::{ResultExt, Snafu}; use crate::{Group, ParseGroupError, ParseVersionError, Version}; @@ -9,6 +7,9 @@ use crate::{Group, ParseGroupError, ParseVersionError, Version}; #[cfg(feature = "serde")] mod serde; +#[cfg(feature = "darling")] +mod darling; + /// Error variants which can be encountered when creating a new [`ApiVersion`] /// from unparsed input. #[derive(Debug, PartialEq, Snafu)] @@ -45,13 +46,13 @@ impl FromStr for ApiVersion { fn from_str(input: &str) -> Result { let (group, version) = if let Some((group, version)) = input.split_once('/') { let group = Group::from_str(group).context(ParseGroupSnafu)?; + let version = Version::from_str(version).context(ParseVersionSnafu)?; - ( - Some(group), - Version::from_str(version).context(ParseVersionSnafu)?, - ) + (Some(group), version) } else { - (None, Version::from_str(input).context(ParseVersionSnafu)?) + let version = Version::from_str(input).context(ParseVersionSnafu)?; + + (None, version) }; Ok(Self { group, version }) @@ -77,13 +78,6 @@ impl Display for ApiVersion { } } -#[cfg(feature = "darling")] -impl FromMeta for ApiVersion { - fn from_string(value: &str) -> darling::Result { - Self::from_str(value).map_err(darling::Error::custom) - } -} - impl ApiVersion { /// Create a new Kubernetes API version. pub fn new(group: Option, version: Version) -> Self { @@ -104,19 +98,11 @@ impl ApiVersion { #[cfg(test)] mod test { - #[cfg(feature = "darling")] - use quote::quote; use rstest::rstest; use super::*; use crate::Level; - #[cfg(feature = "darling")] - fn parse_meta(tokens: proc_macro2::TokenStream) -> ::std::result::Result { - let attribute: syn::Attribute = syn::parse_quote!(#[#tokens]); - Ok(attribute.meta) - } - #[rstest] #[case("extensions/v1beta1", ApiVersion { group: Some("extensions".parse().unwrap()), version: Version { major: 1, level: Some(Level::Beta(1)) } })] #[case("v1beta1", ApiVersion { group: None, version: Version { major: 1, level: Some(Level::Beta(1)) } })] @@ -145,15 +131,4 @@ mod test { fn partial_ord(#[case] input: Version, #[case] other: Version, #[case] expected: Ordering) { assert_eq!(input.partial_cmp(&other), Some(expected)); } - - #[cfg(feature = "darling")] - #[rstest] - #[case(quote!(ignore = "extensions/v1beta1"), ApiVersion { group: Some("extensions".parse().unwrap()), version: Version { major: 1, level: Some(Level::Beta(1)) } })] - #[case(quote!(ignore = "v1beta1"), ApiVersion { group: None, version: Version { major: 1, level: Some(Level::Beta(1)) } })] - #[case(quote!(ignore = "v1"), ApiVersion { group: None, version: Version { major: 1, level: None } })] - fn from_meta(#[case] input: proc_macro2::TokenStream, #[case] expected: ApiVersion) { - let meta = parse_meta(input).expect("valid attribute tokens"); - let api_version = ApiVersion::from_meta(&meta).expect("version must parse from attribute"); - assert_eq!(api_version, expected); - } } diff --git a/crates/k8s-version/src/group.rs b/crates/k8s-version/src/group.rs index ee15f9911..cef7c5ca5 100644 --- a/crates/k8s-version/src/group.rs +++ b/crates/k8s-version/src/group.rs @@ -47,13 +47,13 @@ impl FromStr for Group { ensure!(group.len() <= MAX_GROUP_LENGTH, TooLongSnafu); ensure!(API_GROUP_REGEX.is_match(group), InvalidFormatSnafu); - Ok(Self(group.to_string())) + Ok(Self(group.to_owned())) } } impl fmt::Display for Group { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) + f.write_str(self) } } diff --git a/crates/stackable-operator/src/commons/resources.rs b/crates/stackable-operator/src/commons/resources.rs index eb6f1361c..4f3d1a208 100644 --- a/crates/stackable-operator/src/commons/resources.rs +++ b/crates/stackable-operator/src/commons/resources.rs @@ -38,7 +38,7 @@ //! crates( //! kube_core = "stackable_operator::kube::core", //! k8s_openapi = "stackable_operator::k8s_openapi", -//! schemars = "stackable_operator::schemars" +//! schemars = "stackable_operator::schemars", //! ) //! )] //! #[serde(rename_all = "camelCase")] diff --git a/crates/stackable-operator/src/crd/authentication/core/mod.rs b/crates/stackable-operator/src/crd/authentication/core/mod.rs index 663b8ac78..9287072e3 100644 --- a/crates/stackable-operator/src/crd/authentication/core/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/core/mod.rs @@ -32,7 +32,7 @@ pub mod versioned { crates( kube_core = "kube::core", k8s_openapi = "k8s_openapi", - schemars = "schemars" + schemars = "schemars", ) ))] #[derive( diff --git a/crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs index 7c4185f8b..674ddbd91 100644 --- a/crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs @@ -169,7 +169,7 @@ impl Default for FieldNames { #[cfg(test)] mod tests { use super::*; - use crate::commons::secret_class::SecretClassVolume; + use crate::{commons::secret_class::SecretClassVolume, utils::yaml_from_str_singleton_map}; #[test] fn minimal() { @@ -213,9 +213,7 @@ mod tests { caCert: secretClass: ldap-ca-cert "#; - let deserializer = serde_yaml::Deserializer::from_str(input); - let ldap: AuthenticationProvider = - serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap(); + let ldap: AuthenticationProvider = yaml_from_str_singleton_map(input).unwrap(); assert_eq!(ldap.port(), 42); assert!(ldap.tls.uses_tls()); diff --git a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs index 87b62179e..8193123ec 100644 --- a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs @@ -365,7 +365,7 @@ mod tests { use super::*; use crate::{ config::fragment::validate, product_config_utils::env_vars_from, - product_logging::spec::default_container_log_config, + product_logging::spec::default_container_log_config, utils::yaml_from_str_singleton_map, }; #[test] @@ -435,9 +435,7 @@ mod tests { --git-config: key:value,safe.directory:/safe-dir "#; - let deserializer = serde_yaml::Deserializer::from_str(git_sync_spec); - let git_syncs: Vec = - serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap(); + let git_syncs: Vec = yaml_from_str_singleton_map(git_sync_spec).unwrap(); let resolved_product_image = ResolvedProductImage { image: "oci.stackable.tech/sdp/product:latest".to_string(), diff --git a/crates/stackable-operator/src/crd/listener/mod.rs b/crates/stackable-operator/src/crd/listener/mod.rs index 4820bd299..a199c43f1 100644 --- a/crates/stackable-operator/src/crd/listener/mod.rs +++ b/crates/stackable-operator/src/crd/listener/mod.rs @@ -13,8 +13,8 @@ mod class; mod core; mod listeners; -pub use class::ListenerClass; -pub use listeners::{Listener, PodListeners}; +pub use class::{ListenerClass, ListenerClassVersion}; +pub use listeners::{Listener, ListenerVersion, PodListeners, PodListenersVersion}; // Group all v1alpha1 items in one module. pub mod v1alpha1 { diff --git a/crates/stackable-operator/src/crd/s3/bucket/mod.rs b/crates/stackable-operator/src/crd/s3/bucket/mod.rs index 635961210..335835c84 100644 --- a/crates/stackable-operator/src/crd/s3/bucket/mod.rs +++ b/crates/stackable-operator/src/crd/s3/bucket/mod.rs @@ -21,7 +21,7 @@ pub mod versioned { crates( kube_core = "kube::core", k8s_openapi = "k8s_openapi", - schemars = "schemars" + schemars = "schemars", ), namespaced ))] diff --git a/crates/stackable-operator/src/crd/s3/connection/mod.rs b/crates/stackable-operator/src/crd/s3/connection/mod.rs index 04ace7ee1..97c754e2c 100644 --- a/crates/stackable-operator/src/crd/s3/connection/mod.rs +++ b/crates/stackable-operator/src/crd/s3/connection/mod.rs @@ -31,7 +31,7 @@ pub mod versioned { crates( kube_core = "kube::core", k8s_openapi = "k8s_openapi", - schemars = "schemars" + schemars = "schemars", ), namespaced ))] diff --git a/crates/stackable-operator/src/crd/s3/mod.rs b/crates/stackable-operator/src/crd/s3/mod.rs index ab4a84c21..2f2851a15 100644 --- a/crates/stackable-operator/src/crd/s3/mod.rs +++ b/crates/stackable-operator/src/crd/s3/mod.rs @@ -1,8 +1,8 @@ mod bucket; mod connection; -pub use bucket::S3Bucket; -pub use connection::S3Connection; +pub use bucket::{S3Bucket, S3BucketVersion}; +pub use connection::{S3Connection, S3ConnectionVersion}; // Group all v1alpha1 items in one module. pub mod v1alpha1 { diff --git a/crates/stackable-operator/src/utils/mod.rs b/crates/stackable-operator/src/utils/mod.rs index 12b469b11..b79b1d621 100644 --- a/crates/stackable-operator/src/utils/mod.rs +++ b/crates/stackable-operator/src/utils/mod.rs @@ -25,3 +25,11 @@ pub use self::{option::OptionExt, url::UrlExt}; pub(crate) fn format_full_controller_name(operator: &str, controller: &str) -> String { format!("{operator}_{controller}") } + +pub fn yaml_from_str_singleton_map<'a, D>(input: &'a str) -> Result +where + D: serde::Deserialize<'a>, +{ + let deserializer = serde_yaml::Deserializer::from_str(input); + serde_yaml::with::singleton_map_recursive::deserialize(deserializer) +} diff --git a/crates/stackable-versioned-macros/Cargo.toml b/crates/stackable-versioned-macros/Cargo.toml index 9739c4427..fe6fe91a4 100644 --- a/crates/stackable-versioned-macros/Cargo.toml +++ b/crates/stackable-versioned-macros/Cargo.toml @@ -33,6 +33,7 @@ k8s-version = { path = "../k8s-version", features = ["darling"] } convert_case.workspace = true darling.workspace = true +indoc.workspace = true itertools.workspace = true k8s-openapi = { workspace = true, optional = true } kube = { workspace = true, optional = true } @@ -53,4 +54,5 @@ serde.workspace = true serde_json.workspace = true serde_yaml.workspace = true snafu.workspace = true +tracing.workspace = true trybuild.workspace = true diff --git a/crates/stackable-versioned-macros/src/attrs/container/k8s.rs b/crates/stackable-versioned-macros/src/attrs/container/k8s.rs index a9c17b476..88ca013d4 100644 --- a/crates/stackable-versioned-macros/src/attrs/container/k8s.rs +++ b/crates/stackable-versioned-macros/src/attrs/container/k8s.rs @@ -47,25 +47,10 @@ pub struct KubernetesArguments { // doc // annotation // label - pub skip: Option, - #[darling(default)] pub options: KubernetesConfigOptions, } -/// This struct contains supported kubernetes skip arguments. -/// -/// Supported arguments are: -/// -/// - `merged_crd` flag, which skips generating the `crd()` and `merged_crd()` functions are -/// generated. -#[derive(Clone, Debug, FromMeta)] -pub struct KubernetesSkipArguments { - /// Whether the `crd()` and `merged_crd()` generation should be skipped for - /// this container. - pub merged_crd: Flag, -} - /// This struct contains crate overrides to be passed to `#[kube]`. #[derive(Clone, Debug, FromMeta)] pub struct KubernetesCrateArguments { @@ -176,4 +161,5 @@ impl ToTokens for KubernetesCrateArguments { #[derive(Clone, Default, Debug, FromMeta)] pub struct KubernetesConfigOptions { pub experimental_conversion_tracking: Flag, + pub enable_tracing: Flag, } diff --git a/crates/stackable-versioned-macros/src/codegen/container/enum.rs b/crates/stackable-versioned-macros/src/codegen/container/enum.rs index c9c7d8937..278145435 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/enum.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/enum.rs @@ -141,11 +141,11 @@ impl Enum { // advise against using generic types, but if you have to, avoid removing it in // later versions. let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl(); + let from_enum_ident = &self.common.idents.parameter; let enum_ident = &self.common.idents.original; - let from_enum_ident = &self.common.idents.from; - let for_module_ident = &next_version.ident; - let from_module_ident = &version.ident; + let for_module_ident = &next_version.idents.module; + let from_module_ident = &version.idents.module; let variants: TokenStream = self .variants @@ -201,11 +201,11 @@ impl Enum { match next_version { Some(next_version) => { let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl(); + let from_enum_ident = &self.common.idents.parameter; let enum_ident = &self.common.idents.original; - let from_enum_ident = &self.common.idents.from; - let for_module_ident = &version.ident; - let from_module_ident = &next_version.ident; + let from_module_ident = &next_version.idents.module; + let for_module_ident = &version.idents.module; let variants: TokenStream = self .variants diff --git a/crates/stackable-versioned-macros/src/codegen/container/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/mod.rs index 9d4b73b17..ed72fbe46 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/mod.rs @@ -1,12 +1,12 @@ use darling::{Result, util::IdentString}; use proc_macro2::{Span, TokenStream}; -use quote::quote; +use quote::{format_ident, quote}; use syn::{Attribute, Ident, ItemEnum, ItemStruct, Visibility}; use crate::{ attrs::container::{StandaloneContainerAttributes, k8s::KubernetesArguments}, codegen::{ - VersionDefinition, + KubernetesTokens, VersionDefinition, container::{r#enum::Enum, r#struct::Struct}, }, utils::ContainerIdentExt, @@ -80,57 +80,34 @@ impl Container { } } - /// Generates Kubernetes specific code snippets. + /// Generates Kubernetes specific code for the container. /// - /// This function returns three values: - /// - /// - an enum variant ident, - /// - an enum variant display string, - /// - and a `CustomResource::crd()` call - /// - /// This function only returns `Some` if it is a struct. Enums cannot be used to define - /// Kubernetes custom resources. - pub fn generate_kubernetes_item( - &self, - version: &VersionDefinition, - ) -> Option<(IdentString, String, TokenStream)> { - match self { - Container::Struct(s) => s.generate_kubernetes_item(version), - Container::Enum(_) => None, - } - } - - /// Generates Kubernetes specific code to merge two or more CRDs into one. - /// - /// This function only returns `Some` if it is a struct. Enums cannot be used to define - /// Kubernetes custom resources. - pub fn generate_kubernetes_merge_crds( + /// This includes CRD merging, CRD conversion, and the conversion tracking status struct. + pub fn generate_kubernetes_code( &self, - enum_variant_idents: &[IdentString], - enum_variant_strings: &[String], - fn_calls: &[TokenStream], + versions: &[VersionDefinition], + tokens: &KubernetesTokens, vis: &Visibility, is_nested: bool, ) -> Option { match self { - Container::Struct(s) => s.generate_kubernetes_merge_crds( - enum_variant_idents, - enum_variant_strings, - fn_calls, - vis, - is_nested, - ), + Container::Struct(s) => s.generate_kubernetes_code(versions, tokens, vis, is_nested), Container::Enum(_) => None, } } - pub fn generate_kubernetes_status_struct(&self) -> Option { + /// Generates KUbernetes specific code for individual versions. + pub fn generate_kubernetes_version_items( + &self, + version: &VersionDefinition, + ) -> Option<(TokenStream, IdentString, TokenStream, String)> { match self { - Container::Struct(s) => s.generate_kubernetes_status_struct(), + Container::Struct(s) => s.generate_kubernetes_version_items(version), Container::Enum(_) => None, } } + /// Returns the original ident of the container. pub fn get_original_ident(&self) -> &Ident { match &self { Container::Struct(s) => s.common.idents.original.as_ident(), @@ -190,16 +167,14 @@ impl StandaloneContainer { pub fn generate_tokens(&self) -> TokenStream { let vis = &self.vis; + let mut kubernetes_tokens = KubernetesTokens::default(); let mut tokens = TokenStream::new(); - let mut kubernetes_merge_crds_fn_calls = Vec::new(); - let mut kubernetes_enum_variant_idents = Vec::new(); - let mut kubernetes_enum_variant_strings = Vec::new(); - let mut versions = self.versions.iter().peekable(); while let Some(version) = versions.next() { let container_definition = self.container.generate_definition(version); + let module_ident = &version.idents.module; // NOTE (@Techassi): Using '.copied()' here does not copy or clone the data, but instead // removes one level of indirection of the double reference '&&'. @@ -221,22 +196,16 @@ impl StandaloneContainer { .as_ref() .map(|note| quote! { #[deprecated = #note] }); - // Generate Kubernetes specific code which is placed outside of the container - // definition. - if let Some((enum_variant_ident, enum_variant_string, fn_call)) = - self.container.generate_kubernetes_item(version) - { - kubernetes_merge_crds_fn_calls.push(fn_call); - kubernetes_enum_variant_idents.push(enum_variant_ident); - kubernetes_enum_variant_strings.push(enum_variant_string); + // Generate Kubernetes specific code (for a particular version) which is placed outside + // of the container definition. + if let Some(items) = self.container.generate_kubernetes_version_items(version) { + kubernetes_tokens.push(items); } - let version_ident = &version.ident; - tokens.extend(quote! { #[automatically_derived] #deprecated_attribute - #vis mod #version_ident { + #vis mod #module_ident { use super::*; #container_definition } @@ -246,16 +215,14 @@ impl StandaloneContainer { }); } - tokens.extend(self.container.generate_kubernetes_merge_crds( - &kubernetes_enum_variant_idents, - &kubernetes_enum_variant_strings, - &kubernetes_merge_crds_fn_calls, + // Finally add tokens outside of the container definitions + tokens.extend(self.container.generate_kubernetes_code( + &self.versions, + &kubernetes_tokens, vis, false, )); - tokens.extend(self.container.generate_kubernetes_status_struct()); - tokens } } @@ -263,32 +230,53 @@ impl StandaloneContainer { /// A collection of container idents used for different purposes. #[derive(Debug)] pub struct ContainerIdents { - /// The ident used in the context of Kubernetes specific code. This ident - /// removes the 'Spec' suffix present in the definition container. + /// This ident removes the 'Spec' suffix present in the definition container. + /// This ident is only used in the context of Kubernetes specific code. pub kubernetes: IdentString, + /// This ident uses the base Kubernetes ident to construct an appropriate ident + /// for auto-generated status structs. This ident is only used in the context of + /// Kubernetes specific code. + pub kubernetes_status: IdentString, + + /// This ident uses the base Kubernetes ident to construct an appropriate ident + /// for auto-generated version enums. This enum is used to select the stored + /// api version when merging CRDs. This ident is only used in the context of + /// Kubernetes specific code. + pub kubernetes_version: IdentString, + + // TODO (@Techassi): Add comment + pub kubernetes_parameter: IdentString, + /// The original ident, or name, of the versioned container. pub original: IdentString, - /// The ident used in the [`From`] impl. - pub from: IdentString, + /// The ident used as a parameter. + pub parameter: IdentString, } impl ContainerIdents { pub fn from(ident: Ident, kubernetes_arguments: Option<&KubernetesArguments>) -> Self { - let kubernetes = kubernetes_arguments.map_or_else( - || ident.as_cleaned_kubernetes_ident(), - |options| { - options.kind.as_ref().map_or_else( - || ident.as_cleaned_kubernetes_ident(), - |kind| IdentString::from(Ident::new(kind, Span::call_site())), - ) + let kubernetes = match kubernetes_arguments { + Some(args) => match &args.kind { + Some(kind) => IdentString::from(Ident::new(kind, Span::call_site())), + None => ident.as_cleaned_kubernetes_ident(), }, - ); + None => ident.as_cleaned_kubernetes_ident(), + }; + + let kubernetes_status = + IdentString::from(format_ident!("{kubernetes}StatusWithChangedValues")); + + let kubernetes_version = IdentString::from(format_ident!("{kubernetes}Version")); + let kubernetes_parameter = kubernetes.as_parameter_ident(); Self { - from: ident.as_from_impl_ident(), + parameter: ident.as_parameter_ident(), original: ident.into(), + kubernetes_parameter, + kubernetes_version, + kubernetes_status, kubernetes, } } diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs new file mode 100644 index 000000000..53b3b5361 --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs @@ -0,0 +1,682 @@ +use std::{borrow::Cow, cmp::Ordering, ops::Not as _}; + +use darling::util::IdentString; +use indoc::formatdoc; +use itertools::Itertools as _; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{Visibility, parse_quote}; + +use crate::{ + attrs::container::k8s::KubernetesArguments, + codegen::{KubernetesTokens, VersionDefinition, container::r#struct::Struct}, + utils::{doc_comments::DocComments, path_to_string}, +}; + +const CONVERTED_OBJECT_COUNT_ATTRIBUTE: &str = "k8s.crd.conversion.converted_object_count"; +const DESIRED_API_VERSION_ATTRIBUTE: &str = "k8s.crd.conversion.desired_api_version"; +const API_VERSION_ATTRIBUTE: &str = "k8s.crd.conversion.api_version"; +const STEPS_ATTRIBUTE: &str = "k8s.crd.conversion.steps"; +const KIND_ATTRIBUTE: &str = "k8s.crd.conversion.kind"; + +impl Struct { + pub fn generate_kube_attribute(&self, version: &VersionDefinition) -> Option { + let kubernetes_arguments = self.common.options.kubernetes_arguments.as_ref()?; + + // Required arguments + let group = &kubernetes_arguments.group; + let version = version.inner.to_string(); + let kind = kubernetes_arguments + .kind + .as_ref() + .map_or(self.common.idents.kubernetes.to_string(), |kind| { + kind.clone() + }); + + // Optional arguments + let singular = kubernetes_arguments + .singular + .as_ref() + .map(|s| quote! { , singular = #s }); + + let plural = kubernetes_arguments + .plural + .as_ref() + .map(|p| quote! { , plural = #p }); + + let namespaced = kubernetes_arguments + .namespaced + .is_present() + .then_some(quote! { , namespaced }); + + let crates = &kubernetes_arguments.crates; + + let status = match ( + kubernetes_arguments + .options + .experimental_conversion_tracking + .is_present(), + &kubernetes_arguments.status, + ) { + (true, _) => { + let status_ident = &self.common.idents.kubernetes_status; + Some(quote! { , status = #status_ident }) + } + (_, Some(status_ident)) => Some(quote! { , status = #status_ident }), + (_, _) => None, + }; + + let shortnames: TokenStream = kubernetes_arguments + .shortnames + .iter() + .map(|s| quote! { , shortname = #s }) + .collect(); + + Some(quote! { + // The end-developer needs to derive CustomResource and JsonSchema. + // This is because we don't know if they want to use a re-exported or renamed import. + #[kube( + // 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 + )] + }) + } + + pub fn generate_kubernetes_version_items( + &self, + version: &VersionDefinition, + ) -> Option<(TokenStream, IdentString, TokenStream, String)> { + let kubernetes_arguments = self.common.options.kubernetes_arguments.as_ref()?; + + let module_ident = &version.idents.module; + let struct_ident = &self.common.idents.kubernetes; + + let variant_data = quote! { #module_ident::#struct_ident }; + + let crd_fn = self.generate_kubernetes_crd_fn(version, kubernetes_arguments); + let variant_ident = version.idents.variant.clone(); + let variant_string = version.inner.to_string(); + + Some((crd_fn, variant_ident, variant_data, variant_string)) + } + + fn generate_kubernetes_crd_fn( + &self, + version: &VersionDefinition, + kubernetes_arguments: &KubernetesArguments, + ) -> TokenStream { + let kube_core_path = &*kubernetes_arguments.crates.kube_core; + let struct_ident = &self.common.idents.kubernetes; + let module_ident = &version.idents.module; + + quote! { + <#module_ident::#struct_ident as #kube_core_path::CustomResourceExt>::crd() + } + } + + pub fn generate_kubernetes_code( + &self, + versions: &[VersionDefinition], + tokens: &KubernetesTokens, + vis: &Visibility, + is_nested: bool, + ) -> Option { + let kubernetes_arguments = self.common.options.kubernetes_arguments.as_ref()?; + + // Get various idents needed for code generation + let variant_data_ident = &self.common.idents.kubernetes_parameter; + let version_enum_ident = &self.common.idents.kubernetes_version; + let enum_ident = &self.common.idents.kubernetes; + + // Only add the #[automatically_derived] attribute if this impl is used outside of a + // module (in standalone mode). + let automatically_derived = is_nested.not().then(|| quote! {#[automatically_derived]}); + + // Get the crate paths + let k8s_openapi_path = &*kubernetes_arguments.crates.k8s_openapi; + let serde_json_path = &*kubernetes_arguments.crates.serde_json; + let versioned_path = &*kubernetes_arguments.crates.versioned; + let kube_core_path = &*kubernetes_arguments.crates.kube_core; + + // Get the per-version items to be able to iterate over them via quote + let variant_strings = &tokens.variant_strings; + let variant_idents = &tokens.variant_idents; + let variant_data = &tokens.variant_data; + let crd_fns = &tokens.crd_fns; + + let api_versions = variant_strings + .iter() + .map(|version| format!("{group}/{version}", group = &kubernetes_arguments.group)); + + // Generate additional Kubernetes code, this is split out to reduce the complexity in this + // function. + let status_struct = self.generate_kubernetes_status_struct(kubernetes_arguments, is_nested); + let version_enum = self.generate_kubernetes_version_enum(tokens, vis, is_nested); + let convert_method = self.generate_kubernetes_conversion(versions); + + let parse_object_error = quote! { #versioned_path::ParseObjectError }; + + Some(quote! { + #automatically_derived + #vis enum #enum_ident { + #(#variant_idents(#variant_data)),* + } + + #automatically_derived + impl #enum_ident { + /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. + pub fn merged_crd( + stored_apiversion: #version_enum_ident + ) -> ::std::result::Result< + #k8s_openapi_path::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, + #kube_core_path::crd::MergeError> + { + #kube_core_path::crd::merge_crds(vec![#(#crd_fns),*], stored_apiversion.as_str()) + } + + #convert_method + + fn from_json_value(value: #serde_json_path::Value) -> ::std::result::Result { + let api_version = value + .get("apiVersion") + .ok_or_else(|| #parse_object_error::FieldNotPresent)? + .as_str() + .ok_or_else(|| #parse_object_error::FieldNotStr)?; + + let object = match api_version { + #(#api_versions | #variant_strings => { + let object = #serde_json_path::from_value(value) + .map_err(|source| #parse_object_error::Deserialize { source })?; + + Self::#variant_idents(object) + },)* + unknown_api_version => return ::std::result::Result::Err(#parse_object_error::UnknownApiVersion { + api_version: unknown_api_version.to_owned() + }), + }; + + ::std::result::Result::Ok(object) + } + + fn into_json_value(self) -> ::std::result::Result<#serde_json_path::Value, #serde_json_path::Error> { + match self { + #(Self::#variant_idents(#variant_data_ident) => Ok(#serde_json_path::to_value(#variant_data_ident)?),)* + } + } + } + + #version_enum + #status_struct + }) + } + + //////////////////// + // Merge CRD Code // + //////////////////// + + fn generate_kubernetes_version_enum( + &self, + tokens: &KubernetesTokens, + vis: &Visibility, + is_nested: bool, + ) -> TokenStream { + let enum_ident = &self.common.idents.kubernetes_version; + + // Only add the #[automatically_derived] attribute if this impl is used outside of a + // module (in standalone mode). + let automatically_derived = is_nested.not().then(|| quote! {#[automatically_derived]}); + + // Get the per-version items to be able to iterate over them via quote + let variant_strings = &tokens.variant_strings; + let variant_idents = &tokens.variant_idents; + + quote! { + #automatically_derived + #vis enum #enum_ident { + #(#variant_idents),* + } + + #automatically_derived + impl ::std::fmt::Display for #enum_ident { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> { + f.write_str(self.as_str()) + } + } + + #automatically_derived + impl #enum_ident { + pub fn as_str(&self) -> &str { + match self { + #(#variant_idents => #variant_strings),* + } + } + } + } + } + + ///////////////////////// + // CRD Conversion Code // + ///////////////////////// + + fn generate_kubernetes_status_struct( + &self, + kubernetes_arguments: &KubernetesArguments, + is_nested: bool, + ) -> Option { + kubernetes_arguments + .options + .experimental_conversion_tracking + .is_present() + .then(|| { + let status_ident = &self.common.idents.kubernetes_status; + + let versioned_crate = &*kubernetes_arguments.crates.versioned; + let schemars_crate = &*kubernetes_arguments.crates.schemars; + let serde_crate = &*kubernetes_arguments.crates.serde; + + // Only add the #[automatically_derived] attribute if this impl is used outside of a + // module (in standalone mode). + let automatically_derived = + is_nested.not().then(|| quote! {#[automatically_derived]}); + + // TODO (@Techassi): Validate that users don't specify the status we generate + let status = kubernetes_arguments.status.as_ref().map(|status| { + quote! { + #[serde(flatten)] + pub status: #status, + } + }); + + quote! { + #automatically_derived + #[derive( + ::core::clone::Clone, + ::core::fmt::Debug, + #serde_crate::Deserialize, + #serde_crate::Serialize, + #schemars_crate::JsonSchema + )] + #[serde(rename_all = "camelCase")] + pub struct #status_ident { + pub changed_values: #versioned_crate::ChangedValues, + + #status + } + } + }) + } + + fn generate_kubernetes_conversion( + &self, + versions: &[VersionDefinition], + ) -> Option { + let kubernetes_arguments = self.common.options.kubernetes_arguments.as_ref()?; + + let struct_ident = &self.common.idents.kubernetes; + + let kube_client_path = &*kubernetes_arguments.crates.kube_client; + let serde_json_path = &*kubernetes_arguments.crates.serde_json; + let versioned_path = &*kubernetes_arguments.crates.versioned; + let kube_core_path = &*kubernetes_arguments.crates.kube_core; + + let convert_object_error = quote! { #versioned_path::ConvertObjectError }; + + // Generate conversion paths and the match arms for these paths + let match_arms = + self.generate_kubernetes_conversion_match_arms(versions, kubernetes_arguments); + + // TODO (@Techassi): Make this a feature, drop the option from the macro arguments + // Generate tracing attributes and events if tracing is enabled + let TracingTokens { + successful_conversion_response_event, + convert_objects_instrumentation, + invalid_conversion_review_event, + try_convert_instrumentation, + } = self.generate_kubernetes_conversion_tracing(kubernetes_arguments); + + // Generate doc comments + let conversion_review_reference = + path_to_string(&parse_quote! { #kube_core_path::conversion::ConversionReview }); + + let docs = formatdoc! {" + Tries to convert a list of objects of kind [`{struct_ident}`] 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]: {conversion_review_reference}" + } + .into_doc_comments(); + + Some(quote! { + #(#[doc = #docs])* + #try_convert_instrumentation + pub fn try_convert(review: #kube_core_path::conversion::ConversionReview) + -> #kube_core_path::conversion::ConversionReview + { + // First, turn the review into a conversion request + let request = match #kube_core_path::conversion::ConversionRequest::from_review(review) { + ::std::result::Result::Ok(request) => request, + ::std::result::Result::Err(err) => { + #invalid_conversion_review_event + + return #kube_core_path::conversion::ConversionResponse::invalid( + #kube_client_path::Status { + status: Some(#kube_core_path::response::StatusSummary::Failure), + message: err.to_string(), + reason: err.to_string(), + details: None, + code: 400, + } + ).into_review() + } + }; + + // Extract the desired api version + let desired_api_version = request.desired_api_version.as_str(); + + // Convert all objects into the desired version + let response = match Self::convert_objects(request.objects, desired_api_version) { + ::std::result::Result::Ok(converted_objects) => { + #successful_conversion_response_event + + // We construct the response from the ground up as the helper functions + // don't provide any benefit over manually doing it. Constructing a + // ConversionResponse via for_request is not possible due to a partial move + // of request.objects. The function internally doesn't even use the list of + // objects. The success function on ConversionResponse basically only sets + // the result to success and the converted objects to the provided list. + // The below code does the same thing. + #kube_core_path::conversion::ConversionResponse { + result: #kube_client_path::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_path::conversion::ConversionResponse { + result: #kube_client_path::Status { + status: Some(#kube_core_path::response::StatusSummary::Failure), + message: message.clone(), + reason: message, + details: None, + code, + }, + types: request.types, + uid: request.uid, + converted_objects: vec![], + } + }, + }; + + response.into_review() + } + + #convert_objects_instrumentation + fn convert_objects( + objects: ::std::vec::Vec<#serde_json_path::Value>, + desired_api_version: &str, + ) + -> ::std::result::Result<::std::vec::Vec<#serde_json_path::Value>, #convert_object_error> + { + let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); + + for object in objects { + // This clone is required because in the noop case we move the object into + // the converted objects vec. + let current_object = Self::from_json_value(object.clone()) + .map_err(|source| #convert_object_error::Parse { source })?; + + match (current_object, desired_api_version) { + #(#match_arms,)* + // If no match arm matches, this is a noop. This is the case if the desired + // version matches the current object api version. + // NOTE (@Techassi): I'm curious if this will ever happen? In theory the K8s + // apiserver should never send such a conversion review. + _ => converted_objects.push(object), + } + } + + ::std::result::Result::Ok(converted_objects) + } + }) + } + + fn generate_kubernetes_conversion_match_arms( + &self, + versions: &[VersionDefinition], + kubernetes_arguments: &KubernetesArguments, + ) -> Vec { + let variant_data_ident = &self.common.idents.kubernetes_parameter; + let struct_ident = &self.common.idents.kubernetes; + let spec_ident = &self.common.idents.original; + + let versioned_path = &*kubernetes_arguments.crates.versioned; + let convert_object_error = quote! { #versioned_path::ConvertObjectError }; + + let conversion_paths = conversion_paths(versions); + + conversion_paths + .iter() + .map(|(start, path)| { + let current_object_version_ident = &start.idents.variant; + let current_object_version_string = &start.inner.to_string(); + + let desired_object_version = path.last().expect("the path always contains at least one element"); + let desired_object_version_string = desired_object_version.inner.to_string(); + let desired_object_variant_ident = &desired_object_version.idents.variant; + let desired_object_module_ident = &desired_object_version.idents.module; + + let conversions = path.iter().enumerate().map(|(i, v)| { + let module_ident = &v.idents.module; + + if i == 0 { + quote! { + let converted: #module_ident::#spec_ident = #variant_data_ident.spec.into(); + } + } else { + quote! { + let converted: #module_ident::#spec_ident = converted.into(); + } + } + }); + + let kind = self.common.idents.kubernetes.to_string(); + let steps = path.len(); + + let convert_object_trace = kubernetes_arguments.options.enable_tracing.is_present().then(|| quote! { + ::tracing::trace!( + #DESIRED_API_VERSION_ATTRIBUTE = #desired_object_version_string, + #API_VERSION_ATTRIBUTE = #current_object_version_string, + #STEPS_ATTRIBUTE = #steps, + #KIND_ATTRIBUTE = #kind, + "Successfully converted object" + ); + }); + + // Carry over the status field if the user set a status subresource + let status_field = kubernetes_arguments.status + .is_some() + .then(|| quote! { status: #variant_data_ident.status, }); + + quote! { + (Self::#current_object_version_ident(#variant_data_ident), #desired_object_version_string) => { + #(#conversions)* + + let desired_object = Self::#desired_object_variant_ident(#desired_object_module_ident::#struct_ident { + metadata: #variant_data_ident.metadata, + #status_field + spec: converted, + }); + + let desired_object = desired_object.into_json_value() + .map_err(|source| #convert_object_error::Serialize { source })?; + + #convert_object_trace + + converted_objects.push(desired_object); + } + } + }) + .collect() + } + + fn generate_kubernetes_conversion_tracing( + &self, + kubernetes_arguments: &KubernetesArguments, + ) -> TracingTokens { + if kubernetes_arguments.options.enable_tracing.is_present() { + // TODO (@Techassi): Make tracing path configurable. Currently not possible, needs + // upstream change + let kind = self.common.idents.kubernetes.to_string(); + + let successful_conversion_response_event = Some(quote! { + ::tracing::debug!( + #CONVERTED_OBJECT_COUNT_ATTRIBUTE = converted_objects.len(), + #KIND_ATTRIBUTE = #kind, + "Successfully converted objects" + ); + }); + + let convert_objects_instrumentation = Some(quote! { + #[::tracing::instrument( + skip_all, + err + )] + }); + + let invalid_conversion_review_event = Some(quote! { + ::tracing::warn!(?err, "received invalid conversion review"); + }); + + // NOTE (@Techassi): We sadly cannot use the constants here, because + // the fields only accept idents, which strings are not. + let try_convert_instrumentation = Some(quote! { + #[::tracing::instrument( + skip_all, + fields( + k8s.crd.conversion.api_version = review.types.api_version, + k8s.crd.kind = review.types.kind, + ) + )] + }); + + TracingTokens { + successful_conversion_response_event, + convert_objects_instrumentation, + invalid_conversion_review_event, + try_convert_instrumentation, + } + } else { + TracingTokens::default() + } + } +} + +#[derive(Debug, Default)] +struct TracingTokens { + successful_conversion_response_event: Option, + convert_objects_instrumentation: Option, + invalid_conversion_review_event: Option, + try_convert_instrumentation: Option, +} + +fn conversion_paths(elements: &[T]) -> Vec<(&T, Cow<'_, [T]>)> +where + T: Clone + Ord, +{ + let mut chain = Vec::new(); + + // First, create all 2-permutations of the provided list of elements. It is important + // we select permutations instead of combinations because the order of elements matter. + // A quick example of what the iterator adaptor produces: A list with three elements + // 'v1alpha1', 'v1beta1', and 'v1' will produce six (3! / (3 - 2)!) permutations: + // + // - v1alpha1 -> v1beta1 + // - v1alpha1 -> v1 + // - v1beta1 -> v1 + // - v1beta1 -> v1alpha1 + // - v1 -> v1alpha1 + // - v1 -> v1beta1 + + for pair in elements.iter().permutations(2) { + let start = pair[0]; + let end = pair[1]; + + // Next, we select the positions of the start and end element in the original + // slice. These indices are used to construct the conversion path, which contains + // elements between start (excluding) and the end (including). These elements + // describe the steps needed to go from the start to the end (upgrade or downgrade + // depending on the direction). + if let (Some(start_index), Some(end_index)) = ( + elements.iter().position(|v| v == start), + elements.iter().position(|v| v == end), + ) { + let path = match start_index.cmp(&end_index) { + Ordering::Less => { + // If the start index is smaller than the end index (upgrade), we can return + // a slice pointing directly into the original slice. That's why Cow::Borrowed + // can be used here. + Cow::Borrowed(&elements[start_index + 1..=end_index]) + } + Ordering::Greater => { + // If the start index is bigger than the end index (downgrade), we need to reverse + // the elements. With a slice, this is only possible to do in place, which is not + // what we want in this case. Instead, the data is reversed and cloned and collected + // into a Vec and Cow::Owned is used. + let path = elements[end_index..start_index] + .iter() + .rev() + .cloned() + .collect(); + Cow::Owned(path) + } + Ordering::Equal => unreachable!( + "start and end index cannot be the same due to selecting permutations" + ), + }; + + chain.push((start, path)); + } + } + + chain +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_path_is_the_goal() { + let paths = conversion_paths(&["v1alpha1", "v1alpha2", "v1beta1", "v1"]); + assert_eq!(paths.len(), 12); + + let expected = vec![ + ("v1alpha1", vec!["v1alpha2"]), + ("v1alpha1", vec!["v1alpha2", "v1beta1"]), + ("v1alpha1", vec!["v1alpha2", "v1beta1", "v1"]), + ("v1alpha2", vec!["v1alpha1"]), + ("v1alpha2", vec!["v1beta1"]), + ("v1alpha2", vec!["v1beta1", "v1"]), + ("v1beta1", vec!["v1alpha2", "v1alpha1"]), + ("v1beta1", vec!["v1alpha2"]), + ("v1beta1", vec!["v1"]), + ("v1", vec!["v1beta1", "v1alpha2", "v1alpha1"]), + ("v1", vec!["v1beta1", "v1alpha2"]), + ("v1", vec!["v1beta1"]), + ]; + + for (result, expected) in paths.iter().zip(expected) { + assert_eq!(*result.0, expected.0); + assert_eq!(result.1.to_vec(), expected.1); + } + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs similarity index 59% rename from crates/stackable-versioned-macros/src/codegen/container/struct.rs rename to crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs index c6aaf8873..ae13fe21c 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs @@ -1,9 +1,9 @@ use std::ops::Not; -use darling::{Error, FromAttributes, Result, util::IdentString}; +use darling::{Error, FromAttributes, Result}; use proc_macro2::TokenStream; -use quote::{ToTokens, format_ident, quote}; -use syn::{Generics, ItemStruct, Path, Visibility, parse_quote}; +use quote::quote; +use syn::{Generics, ItemStruct}; use crate::{ attrs::container::NestedContainerAttributes, @@ -13,9 +13,10 @@ use crate::{ container::{CommonContainerData, Container, ContainerIdents, ContainerOptions}, item::VersionedField, }, - utils::VersionExt, }; +mod k8s; + impl Container { pub fn new_standalone_struct( item_struct: ItemStruct, @@ -184,11 +185,11 @@ impl Struct { // advise against using generic types, but if you have to, avoid removing it in // later versions. let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl(); + let from_struct_ident = &self.common.idents.parameter; let struct_ident = &self.common.idents.original; - let from_struct_ident = &self.common.idents.from; - let for_module_ident = &next_version.ident; - let from_module_ident = &version.ident; + let for_module_ident = &next_version.idents.module; + let from_module_ident = &version.idents.module; let fields: TokenStream = self .fields @@ -244,11 +245,11 @@ impl Struct { match next_version { Some(next_version) => { let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl(); + let from_struct_ident = &self.common.idents.parameter; let struct_ident = &self.common.idents.original; - let from_struct_ident = &self.common.idents.from; - let for_module_ident = &version.ident; - let from_module_ident = &next_version.ident; + let from_module_ident = &next_version.idents.module; + let for_module_ident = &version.idents.module; let fields: TokenStream = self .fields @@ -314,204 +315,3 @@ impl Struct { }) } } - -// TODO (@Techassi): Somehow bundle this into one struct which can emit all K8s related code. This -// makes keeping track of interconnected parts easier. -// Kubernetes-specific token generation -impl Struct { - pub fn generate_kube_attribute(&self, version: &VersionDefinition) -> Option { - let kubernetes_arguments = self.common.options.kubernetes_arguments.as_ref()?; - - // Required arguments - let group = &kubernetes_arguments.group; - let version = version.inner.to_string(); - let kind = kubernetes_arguments - .kind - .as_ref() - .map_or(self.common.idents.kubernetes.to_string(), |kind| { - kind.clone() - }); - - // Optional arguments - let singular = kubernetes_arguments - .singular - .as_ref() - .map(|s| quote! { , singular = #s }); - - let plural = kubernetes_arguments - .plural - .as_ref() - .map(|p| quote! { , plural = #p }); - - let namespaced = kubernetes_arguments - .namespaced - .is_present() - .then_some(quote! { , namespaced }); - let crates = kubernetes_arguments.crates.to_token_stream(); - - let status = match ( - kubernetes_arguments - .options - .experimental_conversion_tracking - .is_present(), - &kubernetes_arguments.status, - ) { - (true, _) => { - // TODO (@Techassi): This struct name should be defined once in a single place instead - // of constructing it in two different places which can lead to de-synchronization. - let status_ident = format_ident!( - "{struct_ident}StatusWithChangedValues", - struct_ident = self.common.idents.kubernetes.as_ident() - ); - Some(quote! { , status = #status_ident }) - } - (_, Some(status_ident)) => Some(quote! { , status = #status_ident }), - (_, _) => None, - }; - - let shortnames: TokenStream = kubernetes_arguments - .shortnames - .iter() - .map(|s| quote! { , shortname = #s }) - .collect(); - - Some(quote! { - // The end-developer needs to derive CustomResource and JsonSchema. - // This is because we don't know if they want to use a re-exported or renamed import. - #[kube( - // 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 - )] - }) - } - - pub fn generate_kubernetes_item( - &self, - version: &VersionDefinition, - ) -> Option<(IdentString, String, TokenStream)> { - let kubernetes_arguments = self.common.options.kubernetes_arguments.as_ref()?; - - if !kubernetes_arguments - .skip - .as_ref() - .is_some_and(|s| s.merged_crd.is_present()) - { - let kube_core_crate = &*kubernetes_arguments.crates.kube_core; - - let enum_variant_ident = version.inner.as_variant_ident(); - let enum_variant_string = version.inner.to_string(); - - let struct_ident = &self.common.idents.kubernetes; - let module_ident = &version.ident; - let qualified_path: Path = parse_quote!(#module_ident::#struct_ident); - - let merge_crds_fn_call = quote! { - <#qualified_path as #kube_core_crate::CustomResourceExt>::crd() - }; - - Some((enum_variant_ident, enum_variant_string, merge_crds_fn_call)) - } else { - None - } - } - - pub fn generate_kubernetes_merge_crds( - &self, - enum_variant_idents: &[IdentString], - enum_variant_strings: &[String], - fn_calls: &[TokenStream], - vis: &Visibility, - is_nested: bool, - ) -> Option { - let kubernetes_arguments = self.common.options.kubernetes_arguments.as_ref()?; - - if !kubernetes_arguments - .skip - .as_ref() - .is_some_and(|s| s.merged_crd.is_present()) - { - let enum_ident = &self.common.idents.kubernetes; - - // Only add the #[automatically_derived] attribute if this impl is used outside of a - // module (in standalone mode). - let automatically_derived = is_nested.not().then(|| quote! {#[automatically_derived]}); - - // Get the crate paths - let k8s_openapi_path = &*kubernetes_arguments.crates.k8s_openapi; - let kube_core_path = &*kubernetes_arguments.crates.kube_core; - - Some(quote! { - #automatically_derived - #vis enum #enum_ident { - #(#enum_variant_idents),* - } - - #automatically_derived - impl ::std::fmt::Display for #enum_ident { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> { - match self { - #(Self::#enum_variant_idents => f.write_str(#enum_variant_strings)),* - } - } - } - - #automatically_derived - impl #enum_ident { - /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. - pub fn merged_crd( - stored_apiversion: Self - ) -> ::std::result::Result<#k8s_openapi_path::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, #kube_core_path::crd::MergeError> { - #kube_core_path::crd::merge_crds(vec![#(#fn_calls),*], &stored_apiversion.to_string()) - } - } - }) - } else { - None - } - } - - pub fn generate_kubernetes_status_struct(&self) -> Option { - let kubernetes_arguments = self.common.options.kubernetes_arguments.as_ref()?; - - kubernetes_arguments - .options - .experimental_conversion_tracking - .is_present() - .then(|| { - let status_ident = format_ident!( - "{struct_ident}StatusWithChangedValues", - struct_ident = self.common.idents.kubernetes.as_ident() - ); - - let versioned_crate = &*kubernetes_arguments.crates.versioned; - let schemars_crate = &*kubernetes_arguments.crates.schemars; - let serde_crate = &*kubernetes_arguments.crates.serde; - - // TODO (@Techassi): Validate that users don't specify the status we generate - let status = kubernetes_arguments.status.as_ref().map(|status| { - quote! { - #[serde(flatten)] - pub status: #status, - } - }); - - quote! { - #[derive( - ::core::clone::Clone, - ::core::fmt::Debug, - #serde_crate::Deserialize, - #serde_crate::Serialize, - #schemars_crate::JsonSchema - )] - #[serde(rename_all = "camelCase")] - pub struct #status_ident { - pub changed_values: #versioned_crate::ChangedValues, - - #status - } - } - }) - } -} diff --git a/crates/stackable-versioned-macros/src/codegen/item/variant.rs b/crates/stackable-versioned-macros/src/codegen/item/variant.rs index 54bf34c37..b9c30f750 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/variant.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/variant.rs @@ -146,8 +146,8 @@ impl VersionedVariant { match (change, next_change) { (_, ItemStatus::Addition { .. }) => None, (old, next) => { - let next_version_ident = &next_version.ident; - let old_version_ident = &version.ident; + let next_version_ident = &next_version.idents.module; + let old_version_ident = &version.idents.module; let next_variant_ident = next.get_ident(); let old_variant_ident = old.get_ident(); @@ -166,8 +166,8 @@ impl VersionedVariant { } } None => { - let next_version_ident = &next_version.ident; - let old_version_ident = &version.ident; + let next_version_ident = &next_version.idents.module; + let old_version_ident = &version.idents.module; let variant_ident = &*self.ident; let old = quote! { @@ -200,8 +200,8 @@ impl VersionedVariant { match (change, next_change) { (_, ItemStatus::Addition { .. }) => None, (old, next) => { - let next_version_ident = &next_version.ident; - let old_version_ident = &version.ident; + let next_version_ident = &next_version.idents.module; + let old_version_ident = &version.idents.module; let next_variant_ident = next.get_ident(); let old_variant_ident = old.get_ident(); @@ -220,8 +220,8 @@ impl VersionedVariant { } } None => { - let next_version_ident = &next_version.ident; - let old_version_ident = &version.ident; + let next_version_ident = &next_version.idents.module; + let old_version_ident = &version.idents.module; let variant_ident = &*self.ident; let old = quote! { diff --git a/crates/stackable-versioned-macros/src/codegen/mod.rs b/crates/stackable-versioned-macros/src/codegen/mod.rs index 73f5d5c5c..6c39b2e51 100644 --- a/crates/stackable-versioned-macros/src/codegen/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/mod.rs @@ -1,16 +1,19 @@ use darling::util::IdentString; use k8s_version::Version; -use quote::format_ident; +use proc_macro2::TokenStream; use syn::{Path, Type}; -use crate::attrs::{container::StandaloneContainerAttributes, module::ModuleAttributes}; +use crate::{ + attrs::{container::StandaloneContainerAttributes, module::ModuleAttributes}, + utils::{VersionExt, doc_comments::DocComments}, +}; pub mod changes; pub mod container; pub mod item; pub mod module; -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct VersionDefinition { /// Indicates that the container version is deprecated. pub deprecated: Option, @@ -22,12 +25,24 @@ pub struct VersionDefinition { pub inner: Version, /// The ident of the container. - pub ident: IdentString, + pub idents: VersionIdents, /// Store additional doc-comment lines for this version. pub docs: Vec, } +impl PartialOrd for VersionDefinition { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for VersionDefinition { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.inner.cmp(&other.inner) + } +} + // NOTE (@Techassi): Can we maybe unify these two impls? impl From<&StandaloneContainerAttributes> for Vec { fn from(attributes: &StandaloneContainerAttributes) -> Self { @@ -37,13 +52,16 @@ impl From<&StandaloneContainerAttributes> for Vec { .iter() .map(|v| VersionDefinition { skip_from: v.skip.as_ref().is_some_and(|s| s.from.is_present()), - ident: format_ident!("{version}", version = v.name.to_string()).into(), + idents: VersionIdents { + module: v.name.as_module_ident(), + variant: v.name.as_variant_ident(), + }, deprecated: v.deprecated.as_ref().map(|r#override| { r#override .clone() .unwrap_or(format!("Version {version} is deprecated", version = v.name)) }), - docs: process_docs(&v.doc), + docs: v.doc.as_deref().into_doc_comments(), inner: v.name, }) .collect() @@ -58,19 +76,28 @@ impl From<&ModuleAttributes> for Vec { .iter() .map(|v| VersionDefinition { skip_from: v.skip.as_ref().is_some_and(|s| s.from.is_present()), - ident: format_ident!("{version}", version = v.name.to_string()).into(), + idents: VersionIdents { + module: v.name.as_module_ident(), + variant: v.name.as_variant_ident(), + }, deprecated: v.deprecated.as_ref().map(|r#override| { r#override .clone() .unwrap_or(format!("Version {version} is deprecated", version = v.name)) }), - docs: process_docs(&v.doc), + docs: v.doc.as_deref().into_doc_comments(), inner: v.name, }) .collect() } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VersionIdents { + pub module: IdentString, + pub variant: IdentString, +} + #[derive(Debug, PartialEq)] pub enum ItemStatus { Addition { @@ -113,19 +140,21 @@ impl ItemStatus { } } -/// Converts lines of doc-comments into a trimmed list. -fn process_docs(input: &Option) -> Vec { - if let Some(input) = input { - input - // Trim the leading and trailing whitespace, deleting superfluous - // empty lines. - .trim() - .lines() - // Trim the leading and trailing whitespace on each line that can be - // introduced when the developer indents multi-line comments. - .map(|line| line.trim().to_owned()) - .collect() - } else { - Vec::new() +// This contains all generated Kubernetes tokens for a particular version. +// This struct can then be used to fully generate the combined final Kubernetes code. +#[derive(Debug, Default)] +pub struct KubernetesTokens { + variant_idents: Vec, + variant_data: Vec, + variant_strings: Vec, + crd_fns: Vec, +} + +impl KubernetesTokens { + pub fn push(&mut self, items: (TokenStream, IdentString, TokenStream, String)) { + self.crd_fns.push(items.0); + self.variant_idents.push(items.1); + self.variant_data.push(items.2); + self.variant_strings.push(items.3); } } diff --git a/crates/stackable-versioned-macros/src/codegen/module.rs b/crates/stackable-versioned-macros/src/codegen/module.rs index 96c650fba..a66ea6891 100644 --- a/crates/stackable-versioned-macros/src/codegen/module.rs +++ b/crates/stackable-versioned-macros/src/codegen/module.rs @@ -7,11 +7,9 @@ use syn::{Ident, Item, ItemMod, ItemUse, Visibility, token::Pub}; use crate::{ ModuleAttributes, - codegen::{VersionDefinition, container::Container}, + codegen::{KubernetesTokens, VersionDefinition, container::Container}, }; -pub type KubernetesItems = (Vec, Vec, Vec); - /// A versioned module. /// /// Versioned modules allow versioning multiple containers at once without introducing conflicting @@ -72,7 +70,7 @@ impl Module { Item::Mod(submodule) => { if !versions .iter() - .any(|v| v.ident.as_ident() == &submodule.ident) + .any(|v| v.idents.module.as_ident() == &submodule.ident) { errors.push( Error::custom( @@ -147,7 +145,7 @@ impl Module { let mut kubernetes_tokens = TokenStream::new(); let mut tokens = TokenStream::new(); - let mut kubernetes_container_items: HashMap = HashMap::new(); + let mut kubernetes_container_items: HashMap = HashMap::new(); let mut versions = self.versions.iter().peekable(); while let Some(version) = versions.next() { @@ -155,7 +153,7 @@ impl Module { let mut container_definitions = TokenStream::new(); let mut from_impls = TokenStream::new(); - let version_ident = &version.ident; + let version_module_ident = &version.idents.module; for container in &self.containers { container_definitions.extend(container.generate_definition(version)); @@ -176,16 +174,12 @@ impl Module { // Generate Kubernetes specific code which is placed outside of the container // definition. - if let Some((enum_variant_ident, enum_variant_string, fn_call)) = - container.generate_kubernetes_item(version) - { + if let Some(items) = container.generate_kubernetes_version_items(version) { let entry = kubernetes_container_items .entry(container.get_original_ident().clone()) .or_default(); - entry.0.push(fn_call); - entry.1.push(enum_variant_ident); - entry.2.push(enum_variant_string); + entry.push(items); } } @@ -207,7 +201,7 @@ impl Module { tokens.extend(quote! { #automatically_derived #deprecated_attribute - #version_module_vis mod #version_ident { + #version_module_vis mod #version_module_ident { use super::*; #submodule_imports @@ -222,21 +216,13 @@ impl Module { // Generate the final Kubernetes specific code for each container (which uses Kubernetes // specific features) which is appended to the end of container definitions. for container in &self.containers { - if let Some(( - kubernetes_merge_crds_fn_calls, - kubernetes_enum_variant_idents, - kubernetes_enum_variant_strings, - )) = kubernetes_container_items.get(container.get_original_ident()) - { - kubernetes_tokens.extend(container.generate_kubernetes_merge_crds( - kubernetes_enum_variant_idents, - kubernetes_enum_variant_strings, - kubernetes_merge_crds_fn_calls, + if let Some(items) = kubernetes_container_items.get(container.get_original_ident()) { + kubernetes_tokens.extend(container.generate_kubernetes_code( + &self.versions, + items, version_module_vis, self.preserve_module, )); - - kubernetes_tokens.extend(container.generate_kubernetes_status_struct()); } } @@ -259,10 +245,12 @@ impl Module { /// Optionally generates imports (which can be re-exports) located in submodules for the /// specified `version`. fn generate_submodule_imports(&self, version: &VersionDefinition) -> Option { - self.submodules.get(&version.ident).map(|use_statements| { - quote! { - #(#use_statements)* - } - }) + self.submodules + .get(&version.idents.module) + .map(|use_statements| { + quote! { + #(#use_statements)* + } + }) } } diff --git a/crates/stackable-versioned-macros/src/lib.rs b/crates/stackable-versioned-macros/src/lib.rs index 9f013a6db..a028be25e 100644 --- a/crates/stackable-versioned-macros/src/lib.rs +++ b/crates/stackable-versioned-macros/src/lib.rs @@ -16,8 +16,6 @@ mod utils; /// This macro enables generating versioned structs and enums. /// -/// # Usage Guide -/// /// In this guide, code blocks usually come in pairs. The first code block /// describes how the macro is used. The second expandable block displays the /// generated piece of code for explanation purposes. It should be noted, that @@ -36,7 +34,7 @@ mod utils; /// /// /// -/// ## Declaring Versions +/// # Declaring Versions /// /// Before any of the fields or variants can be versioned, versions need to be /// declared at the container level. Each version currently supports two @@ -90,7 +88,7 @@ mod utils; /// ``` /// /// -/// ### Deprecation of a Version +/// ## Deprecation of a Version /// /// The `deprecated` flag marks the version as deprecated. This currently adds /// the `#[deprecated]` attribute to the appropriate piece of code. @@ -121,12 +119,19 @@ mod utils; /// ``` /// /// -/// ### Version Sorting +/// ## Version Sorting /// /// Additionally, it is ensured that each version is unique. Declaring the same /// version multiple times will result in an error. Furthermore, declaring the /// versions out-of-order is prohibited by default. It is possible to opt-out -/// of this check by setting `options(allow_unsorted)`: +/// of this check by setting `options(allow_unsorted)`. +/// +///
+/// +/// It is **not** recommended to use this setting and instead use sorted versions +/// across all versioned items. +/// +///
/// /// ``` /// # use stackable_versioned_macros::versioned; @@ -140,7 +145,7 @@ mod utils; /// } /// ``` /// -/// ## Versioning Items in a Module +/// # Versioning Items in a Module /// /// Using the macro on structs and enums is explained in detail in the following /// sections. This section is dedicated to explain the usage of the macro when @@ -237,7 +242,7 @@ mod utils; /// **not** at the struct / enum level. Item actions describes in the following /// section can be used as expected. /// -/// ### Preserve Module +/// ## Preserve Module /// /// The previous examples completely replaced the `versioned` module with /// top-level version modules. This is the default behaviour. Preserving the @@ -261,7 +266,7 @@ mod utils; /// } /// ``` /// -/// ### Re-emitting and merging Submodules +/// ## Re-emitting and merging Submodules /// /// Modules defined in the versioned module will be re-emitted. This allows for /// composition of re-exports to compose easier to use imports for downstream @@ -328,7 +333,7 @@ mod utils; /// /// /// -/// ## Item Actions +/// # Item Actions /// /// This crate currently supports three different item actions. Items can /// be added, changed, and deprecated. The macro ensures that these actions @@ -350,7 +355,7 @@ mod utils; /// removing fields in CRDs entirely. Instead, they should be marked as /// deprecated. By convention this is done with the `deprecated` prefix. /// -/// ### Added Action +/// ## Added Action /// /// This action indicates that an item is added in a particular version. /// Available parameters are: @@ -408,7 +413,7 @@ mod utils; /// ``` /// /// -/// #### Custom Default Function +/// ### Custom Default Function /// /// To customize the default function used in the generated `From` implementation /// you can use the `default` parameter. It expects a path to a function without @@ -454,7 +459,7 @@ mod utils; /// ``` /// /// -/// ### Changed Action +/// ## Changed Action /// /// This action indicates that an item is changed in a particular version. It /// combines renames and type changes into a single action. You can choose to @@ -529,7 +534,7 @@ mod utils; /// ``` /// /// -/// ### Deprecated Action +/// ## Deprecated Action /// /// This action indicates that an item is deprecated in a particular version. /// Deprecated items are not removed. @@ -585,7 +590,7 @@ mod utils; /// ``` /// /// -/// ## Auto-generated `From` Implementations +/// # Auto-generated `From` Implementations /// /// To enable smooth container version upgrades, the macro automatically /// generates `From` implementations. On a high level, code generated for two @@ -659,7 +664,7 @@ mod utils; /// /// /// -/// ### Skipping at the Container Level +/// ## Skipping at the Container Level /// /// Disabling this behavior at the container level results in no `From` /// implementation for all versions. @@ -682,7 +687,7 @@ mod utils; /// } /// ``` /// -/// ### Skipping at the Version Level +/// ## Skipping at the Version Level /// /// Disabling this behavior at the version level results in no `From` /// implementation for that particular version. This can be read as "skip @@ -709,7 +714,7 @@ mod utils; #[cfg_attr( feature = "k8s", doc = r#" -## Kubernetes-specific Features +# Kubernetes-specific Features This macro also offers support for Kubernetes-specific versioning, especially for CustomResourceDefinitions (CRDs). These features are @@ -718,6 +723,8 @@ optional dependencies) and use the `k8s()` parameter in the macro. You need to derive both [`kube::CustomResource`] and [`schemars::JsonSchema`][1]. +## Simple Versioning + ``` # use stackable_versioned_macros::versioned; use kube::CustomResource; @@ -734,7 +741,12 @@ use serde::{Deserialize, Serialize}; pub struct FooSpec { #[versioned( added(since = "v1beta1"), - changed(since = "v1", from_name = "prev_bar", from_type = "u16", downgrade_with = usize_to_u16) + changed( + since = "v1", + from_name = "prev_bar", + from_type = "u16", + downgrade_with = usize_to_u16 + ) )] bar: usize, baz: bool, @@ -743,34 +755,10 @@ pub struct FooSpec { fn usize_to_u16(input: usize) -> u16 { input.try_into().unwrap() } - -# fn main() { -let merged_crd = Foo::merged_crd(Foo::V1).unwrap(); -println!("{}", serde_yaml::to_string(&merged_crd).unwrap()); -# } +# fn main() {} ``` -The generated `merged_crd` method is a wrapper around [kube's `merge_crds`][2] -function. It automatically calls the `crd` methods of the CRD in all of its -versions and additionally provides a strongly typed selector for the stored -API version. - -Currently, the following arguments are supported: - -- `group`: Set the group of the CR object, usually the domain of the company. - This argument is Required. -- `kind`: Override the kind field of the CR object. This defaults to the struct - name (without the 'Spec' suffix). -- `singular`: Set the singular name of the CR object. -- `plural`: Set the plural name of the CR object. -- `namespaced`: Indicate that this is a namespaced scoped resource rather than a - cluster scoped resource. -- `crates`: Override specific crates. -- `status`: Set the specified struct as the status subresource. -- `shortname`: Set a shortname for the CR object. This can be specified multiple - times. - -### Versioning Items in a Module +## Versioning Items in a Module Versioning multiple CRD related structs via a module is supported and common rules from [above](#versioning-items-in-a-module) apply here as well. It should @@ -800,11 +788,7 @@ mod versioned { baz: String, } } - -# fn main() { -let merged_crd = Foo::merged_crd(Foo::V1).unwrap(); -println!("{}", serde_yaml::to_string(&merged_crd).unwrap()); -# } +# fn main() {} ```
@@ -834,8 +818,6 @@ mod v1alpha1 { } } -// Automatic From implementations for conversion between versions ... - mod v1 { use super::*; #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] @@ -858,8 +840,6 @@ mod v1 { pub bar: usize, } } - -// Implementations to create the merged CRDs ... ```
@@ -867,6 +847,150 @@ It is possible to include structs and enums which are not CRDs. They are instead versioned as expected (without adding the `#[kube]` derive macro and generating code to merge CRD versions). +## Arguments + +Currently, the following Kubernetes (kube) specific arguments are supported + +### `#[versioned(k8s(group = "..."))]` + +**Required.** Set the group of the CRD, usually the domain of the company, like +`example.com`. + +### `#[versioned(k8s(kind = "..."))]` + +Override the kind field of the CRD. This defaults to the struct name +(without the `Spec` suffix). Overriding this value will also influence the names +of other generated items, like the status struct (if used) or the version enum. + +### `#[versioned(k8s(singular = "..."))]` + +Set the singular name. Defaults to lowercased `kind` value. + +### `#[versioned(k8s(plural = "..."))]` + +Set the plural name. Defaults to inferring from singular. + +### `#[versioned(k8s(namespaced))]` + +Indicate that this is a namespaced scoped resource rather than a cluster scoped +resource. + +### `#[versioned(k8s(crates(...)))]` + +Override the import path of specific crates. The following code block depicts +supported overrides and their default values. + +```ignore +#[versioned(k8s(crates( + kube_core = ::kube::core, + kube_client = ::kube::client, + k8s_openapi = ::k8s_openapi, + schemars = ::schemars, + serde = ::serde, + serde_json = ::serde_json, + versioned = ::stackable_versioned, +)))] +pub struct Foo {} +``` + +### `#[versioned(k8s(status = "..."))]` + +Set the specified struct as the status subresource. If conversion tracking is +enabled, this struct will be automatically merged into the generated tracking +status struct. + +### `#[versioned(k8s(shortname = "..."))]` + +Set a shortname. This can be specified multiple times. + +### `#[versioned(k8s(options(...)))]` + +```ignore +#[versioned(k8s(options( + // Highly experimental conversion tracking. Opting into this feature will + // introduce frequent breaking changes. + experimental_conversion_tracking, + + // Enables instrumentation and log events via the tracing crate. + enable_tracing, +)))] +pub struct Foo {} +``` + +## Merge CRDs + +The generated `merged_crd` method is a wrapper around [kube's `merge_crds`][2] +function. It automatically calls the `crd` methods of the CRD in all of its +versions and additionally provides a strongly typed selector for the stored +API version. + +``` +# use stackable_versioned_macros::versioned; +# use kube::CustomResource; +# use schemars::JsonSchema; +# use serde::{Deserialize, Serialize}; +#[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1"), + k8s(group = "example.com") +)] +#[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] +pub struct FooSpec { + #[versioned(added(since = "v1beta1"))] + bar: usize, + baz: bool, +} + +# fn main() { +let merged_crd = Foo::merged_crd(FooVersion::V1Beta1).unwrap(); +println!("{yaml}", yaml = serde_yaml::to_string(&merged_crd).unwrap()); +# } +``` + +## Convert CRDs + +The conversion of CRDs is tightly integrated with ConversionReviews, the payload +which a conversion webhook receives from the K8s apiserver. Naturally, the +`try_convert` function takes in ConversionReview as a parameter and also returns +a ConversionReview indicating success or failure. + +```ignore +# use stackable_versioned_macros::versioned; +# use kube::CustomResource; +# use schemars::JsonSchema; +# use serde::{Deserialize, Serialize}; +#[versioned( + version(name = "v1alpha1"), + version(name = "v1beta1"), + k8s(group = "example.com") +)] +#[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] +pub struct FooSpec { + #[versioned(added(since = "v1beta1"))] + bar: usize, + baz: bool, +} + +# fn main() { +let conversion_review = Foo::try_convert(conversion_review); +# } +``` + +## OpenTelemetry Semantic Conventions + +If tracing is enabled, various traces and events are emitted. The fields of these +signals follow the general rules of OpenTelemetry semantic conventions. There are +currently no agreed-upon semantic conventions for CRD conversions. In the meantime +these fields are used: + +| Field | Type (Example) | Description | +| :---- | :------------- | :---------- | +| `k8s.crd.conversion.converted_object_count` | usize (6) | The number of successfully converted objects sent back in a conversion review | +| `k8s.crd.conversion.desired_api_version` | String (v1alpha1) | The desired api version received via a conversion review | +| `k8s.crd.conversion.api_version` | String (v1beta1) | The current api version of an object received via a conversion review | +| `k8s.crd.conversion.steps` | usize (2) | The number of steps required to convert a single object from the current to the desired version | +| `k8s.crd.conversion.kind` | String (Foo) | The kind of the CRD | + [1]: https://docs.rs/schemars/latest/schemars/derive.JsonSchema.html [2]: https://docs.rs/kube/latest/kube/core/crd/fn.merge_crds.html "# diff --git a/crates/stackable-versioned-macros/src/utils/doc_comments.rs b/crates/stackable-versioned-macros/src/utils/doc_comments.rs new file mode 100644 index 000000000..bc4cbfe77 --- /dev/null +++ b/crates/stackable-versioned-macros/src/utils/doc_comments.rs @@ -0,0 +1,25 @@ +pub trait DocComments { + /// Converts lines of doc-comments into a trimmed list which can be expanded via repetition in + /// [`quote::quote`]. + fn into_doc_comments(self) -> Vec; +} + +impl DocComments for &str { + fn into_doc_comments(self) -> Vec { + self + // Trim the leading and trailing whitespace, deleting superfluous + // empty lines. + .trim() + .lines() + // Trim the leading and trailing whitespace on each line that can be + // introduced when the developer indents multi-line comments. + .map(|line| line.trim().to_owned()) + .collect() + } +} + +impl DocComments for Option<&str> { + fn into_doc_comments(self) -> Vec { + self.map_or(vec![], |s| s.into_doc_comments()) + } +} diff --git a/crates/stackable-versioned-macros/src/utils.rs b/crates/stackable-versioned-macros/src/utils/mod.rs similarity index 72% rename from crates/stackable-versioned-macros/src/utils.rs rename to crates/stackable-versioned-macros/src/utils/mod.rs index a682f581b..50a255673 100644 --- a/crates/stackable-versioned-macros/src/utils.rs +++ b/crates/stackable-versioned-macros/src/utils/mod.rs @@ -3,16 +3,27 @@ use std::ops::Deref; use convert_case::{Case, Casing}; use darling::util::IdentString; use k8s_version::Version; +use proc_macro2::Span; use quote::{ToTokens, format_ident}; -use syn::{Ident, spanned::Spanned}; +use syn::{Ident, Path, spanned::Spanned}; + +pub mod doc_comments; pub trait VersionExt { fn as_variant_ident(&self) -> IdentString; + fn as_module_ident(&self) -> IdentString; } impl VersionExt for Version { fn as_variant_ident(&self) -> IdentString { - format_ident!("{ident}", ident = self.to_string().to_case(Case::Pascal)).into() + IdentString::new(Ident::new( + &self.to_string().to_case(Case::Pascal), + Span::call_site(), + )) + } + + fn as_module_ident(&self) -> IdentString { + IdentString::new(Ident::new(&self.to_string(), Span::call_site())) } } @@ -22,7 +33,7 @@ pub trait ContainerIdentExt { fn as_cleaned_kubernetes_ident(&self) -> IdentString; /// Transforms the [`IdentString`] into one usable in the [`From`] impl. - fn as_from_impl_ident(&self) -> IdentString; + fn as_parameter_ident(&self) -> IdentString; } impl ContainerIdentExt for Ident { @@ -31,12 +42,22 @@ impl ContainerIdentExt for Ident { IdentString::new(ident) } - fn as_from_impl_ident(&self) -> IdentString { + fn as_parameter_ident(&self) -> IdentString { let ident = format_ident!("__sv_{}", self.to_string().to_lowercase()); IdentString::new(ident) } } +impl ContainerIdentExt for IdentString { + fn as_cleaned_kubernetes_ident(&self) -> IdentString { + self.as_ident().as_cleaned_kubernetes_ident() + } + + fn as_parameter_ident(&self) -> IdentString { + self.as_ident().as_parameter_ident() + } +} + pub trait ItemIdentExt: Deref + From + Spanned { const DEPRECATED_PREFIX: &'static str; @@ -115,3 +136,17 @@ impl ToTokens for VariantIdent { self.0.to_tokens(tokens); } } + +pub fn path_to_string(path: &Path) -> String { + let pretty_path = path + .segments + .iter() + .map(|s| s.ident.to_string()) + .collect::>() + .join("::"); + + match path.leading_colon { + Some(_) => format!("::{}", pretty_path), + None => pretty_path, + } +} diff --git a/crates/stackable-versioned-macros/tests/inputs/k8s/pass/skip.rs b/crates/stackable-versioned-macros/tests/inputs/k8s/pass/skip.rs deleted file mode 100644 index 535091c9e..000000000 --- a/crates/stackable-versioned-macros/tests/inputs/k8s/pass/skip.rs +++ /dev/null @@ -1,19 +0,0 @@ -use stackable_versioned::versioned; -// --- -#[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1"), - version(name = "v1"), - k8s(group = "stackable.tech", skip(merged_crd)) -)] -// --- -#[derive( - Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema, kube::CustomResource, -)] -pub struct FooSpec { - #[versioned(added(since = "v1beta1"), changed(since = "v1", from_name = "bah"))] - bar: usize, - baz: bool, -} -// --- -fn main() {} diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@basic.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@basic.rs.snap index aa26c0337..24b00a58d 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@basic.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@basic.rs.snap @@ -114,28 +114,15 @@ pub(crate) mod v1 { } #[automatically_derived] pub(crate) enum Foo { - V1Alpha1, - V1Beta1, - V1, -} -#[automatically_derived] -impl ::std::fmt::Display for Foo { - fn fmt( - &self, - f: &mut ::std::fmt::Formatter<'_>, - ) -> ::std::result::Result<(), ::std::fmt::Error> { - match self { - Self::V1Alpha1 => f.write_str("v1alpha1"), - Self::V1Beta1 => f.write_str("v1beta1"), - Self::V1 => f.write_str("v1"), - } - } + V1Alpha1(v1alpha1::Foo), + V1Beta1(v1beta1::Foo), + V1(v1::Foo), } #[automatically_derived] impl Foo { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( - stored_apiversion: Self, + stored_apiversion: FooVersion, ) -> ::std::result::Result< ::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, ::kube::core::crd::MergeError, @@ -146,7 +133,241 @@ impl Foo { v1beta1::Foo as ::kube::core::CustomResourceExt > ::crd(), < v1::Foo as ::kube::core::CustomResourceExt > ::crd() ], - &stored_apiversion.to_string(), + stored_apiversion.as_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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: err.to_string(), + reason: err.to_string(), + details: None, + code: 400, + }) + .into_review(); + } + }; + let desired_api_version = request.desired_api_version.as_str(); + let response = match Self::convert_objects( + request.objects, + desired_api_version, + ) { + ::std::result::Result::Ok(converted_objects) => { + ::kube::core::conversion::ConversionResponse { + result: ::kube::client::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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: message.clone(), + 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 mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); + for object in objects { + let current_object = Self::from_json_value(object.clone()) + .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { + source, + })?; + match (current_object, desired_api_version) { + (Self::V1Alpha1(__sv_foo), "v1beta1") => { + let converted: v1beta1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1Beta1(v1beta1::Foo { + metadata: __sv_foo.metadata, + status: __sv_foo.status, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Alpha1(__sv_foo), "v1") => { + let converted: v1beta1::FooSpec = __sv_foo.spec.into(); + let converted: v1::FooSpec = converted.into(); + let desired_object = Self::V1(v1::Foo { + metadata: __sv_foo.metadata, + status: __sv_foo.status, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Beta1(__sv_foo), "v1alpha1") => { + let converted: v1alpha1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1Alpha1(v1alpha1::Foo { + metadata: __sv_foo.metadata, + status: __sv_foo.status, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Beta1(__sv_foo), "v1") => { + let converted: v1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1(v1::Foo { + metadata: __sv_foo.metadata, + status: __sv_foo.status, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_foo), "v1alpha1") => { + let converted: v1beta1::FooSpec = __sv_foo.spec.into(); + let converted: v1alpha1::FooSpec = converted.into(); + let desired_object = Self::V1Alpha1(v1alpha1::Foo { + metadata: __sv_foo.metadata, + status: __sv_foo.status, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_foo), "v1beta1") => { + let converted: v1beta1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1Beta1(v1beta1::Foo { + metadata: __sv_foo.metadata, + status: __sv_foo.status, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + _ => converted_objects.push(object), + } + } + ::std::result::Result::Ok(converted_objects) + } + fn from_json_value( + value: ::serde_json::Value, + ) -> ::std::result::Result { + let api_version = value + .get("apiVersion") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + let object = match api_version { + "stackable.tech/v1alpha1" | "v1alpha1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Alpha1(object) + } + "stackable.tech/v1beta1" | "v1beta1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Beta1(object) + } + "stackable.tech/v1" | "v1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1(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)?), + Self::V1Beta1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + Self::V1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + } + } +} +#[automatically_derived] +pub(crate) enum FooVersion { + V1Alpha1, + V1Beta1, + V1, +} +#[automatically_derived] +impl ::std::fmt::Display for FooVersion { + fn fmt( + &self, + f: &mut ::std::fmt::Formatter<'_>, + ) -> ::std::result::Result<(), ::std::fmt::Error> { + f.write_str(self.as_str()) + } +} +#[automatically_derived] +impl FooVersion { + pub fn as_str(&self) -> &str { + match self { + V1Alpha1 => "v1alpha1", + V1Beta1 => "v1beta1", + V1 => "v1", + } + } } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@conversion_tracking.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@conversion_tracking.rs.snap index 9aa32539b..9b72f1188 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@conversion_tracking.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@conversion_tracking.rs.snap @@ -84,28 +84,15 @@ pub(crate) mod v1 { } #[automatically_derived] pub(crate) enum Foo { - V1Alpha1, - V1Beta1, - V1, -} -#[automatically_derived] -impl ::std::fmt::Display for Foo { - fn fmt( - &self, - f: &mut ::std::fmt::Formatter<'_>, - ) -> ::std::result::Result<(), ::std::fmt::Error> { - match self { - Self::V1Alpha1 => f.write_str("v1alpha1"), - Self::V1Beta1 => f.write_str("v1beta1"), - Self::V1 => f.write_str("v1"), - } - } + V1Alpha1(v1alpha1::Foo), + V1Beta1(v1beta1::Foo), + V1(v1::Foo), } #[automatically_derived] impl Foo { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( - stored_apiversion: Self, + stored_apiversion: FooVersion, ) -> ::std::result::Result< ::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, ::kube::core::crd::MergeError, @@ -116,10 +103,245 @@ impl Foo { v1beta1::Foo as ::kube::core::CustomResourceExt > ::crd(), < v1::Foo as ::kube::core::CustomResourceExt > ::crd() ], - &stored_apiversion.to_string(), + stored_apiversion.as_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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: err.to_string(), + reason: err.to_string(), + details: None, + code: 400, + }) + .into_review(); + } + }; + let desired_api_version = request.desired_api_version.as_str(); + let response = match Self::convert_objects( + request.objects, + desired_api_version, + ) { + ::std::result::Result::Ok(converted_objects) => { + ::kube::core::conversion::ConversionResponse { + result: ::kube::client::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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: message.clone(), + 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 mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); + for object in objects { + let current_object = Self::from_json_value(object.clone()) + .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { + source, + })?; + match (current_object, desired_api_version) { + (Self::V1Alpha1(__sv_foo), "v1beta1") => { + let converted: v1beta1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1Beta1(v1beta1::Foo { + metadata: __sv_foo.metadata, + status: __sv_foo.status, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Alpha1(__sv_foo), "v1") => { + let converted: v1beta1::FooSpec = __sv_foo.spec.into(); + let converted: v1::FooSpec = converted.into(); + let desired_object = Self::V1(v1::Foo { + metadata: __sv_foo.metadata, + status: __sv_foo.status, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Beta1(__sv_foo), "v1alpha1") => { + let converted: v1alpha1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1Alpha1(v1alpha1::Foo { + metadata: __sv_foo.metadata, + status: __sv_foo.status, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Beta1(__sv_foo), "v1") => { + let converted: v1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1(v1::Foo { + metadata: __sv_foo.metadata, + status: __sv_foo.status, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_foo), "v1alpha1") => { + let converted: v1beta1::FooSpec = __sv_foo.spec.into(); + let converted: v1alpha1::FooSpec = converted.into(); + let desired_object = Self::V1Alpha1(v1alpha1::Foo { + metadata: __sv_foo.metadata, + status: __sv_foo.status, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_foo), "v1beta1") => { + let converted: v1beta1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1Beta1(v1beta1::Foo { + metadata: __sv_foo.metadata, + status: __sv_foo.status, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + _ => converted_objects.push(object), + } + } + ::std::result::Result::Ok(converted_objects) + } + fn from_json_value( + value: ::serde_json::Value, + ) -> ::std::result::Result { + let api_version = value + .get("apiVersion") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + let object = match api_version { + "stackable.tech/v1alpha1" | "v1alpha1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Alpha1(object) + } + "stackable.tech/v1beta1" | "v1beta1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Beta1(object) + } + "stackable.tech/v1" | "v1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1(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)?), + Self::V1Beta1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + Self::V1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + } + } +} +#[automatically_derived] +pub(crate) enum FooVersion { + V1Alpha1, + V1Beta1, + V1, } +#[automatically_derived] +impl ::std::fmt::Display for FooVersion { + fn fmt( + &self, + f: &mut ::std::fmt::Formatter<'_>, + ) -> ::std::result::Result<(), ::std::fmt::Error> { + f.write_str(self.as_str()) + } +} +#[automatically_derived] +impl FooVersion { + pub fn as_str(&self) -> &str { + match self { + V1Alpha1 => "v1alpha1", + V1Beta1 => "v1beta1", + V1 => "v1", + } + } +} +#[automatically_derived] #[derive( ::core::clone::Clone, ::core::fmt::Debug, diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@crate_overrides.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@crate_overrides.rs.snap index cf7afc27a..3853ad39f 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@crate_overrides.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@crate_overrides.rs.snap @@ -105,28 +105,15 @@ pub mod v1 { } #[automatically_derived] pub enum Foo { - V1Alpha1, - V1Beta1, - V1, -} -#[automatically_derived] -impl ::std::fmt::Display for Foo { - fn fmt( - &self, - f: &mut ::std::fmt::Formatter<'_>, - ) -> ::std::result::Result<(), ::std::fmt::Error> { - match self { - Self::V1Alpha1 => f.write_str("v1alpha1"), - Self::V1Beta1 => f.write_str("v1beta1"), - Self::V1 => f.write_str("v1"), - } - } + V1Alpha1(v1alpha1::Foo), + V1Beta1(v1beta1::Foo), + V1(v1::Foo), } #[automatically_derived] impl Foo { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( - stored_apiversion: Self, + stored_apiversion: FooVersion, ) -> ::std::result::Result< ::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, ::kube::core::crd::MergeError, @@ -137,7 +124,235 @@ impl Foo { v1beta1::Foo as ::kube::core::CustomResourceExt > ::crd(), < v1::Foo as ::kube::core::CustomResourceExt > ::crd() ], - &stored_apiversion.to_string(), + stored_apiversion.as_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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: err.to_string(), + reason: err.to_string(), + details: None, + code: 400, + }) + .into_review(); + } + }; + let desired_api_version = request.desired_api_version.as_str(); + let response = match Self::convert_objects( + request.objects, + desired_api_version, + ) { + ::std::result::Result::Ok(converted_objects) => { + ::kube::core::conversion::ConversionResponse { + result: ::kube::client::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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: message.clone(), + 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 mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); + for object in objects { + let current_object = Self::from_json_value(object.clone()) + .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { + source, + })?; + match (current_object, desired_api_version) { + (Self::V1Alpha1(__sv_foo), "v1beta1") => { + let converted: v1beta1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1Beta1(v1beta1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Alpha1(__sv_foo), "v1") => { + let converted: v1beta1::FooSpec = __sv_foo.spec.into(); + let converted: v1::FooSpec = converted.into(); + let desired_object = Self::V1(v1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Beta1(__sv_foo), "v1alpha1") => { + let converted: v1alpha1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1Alpha1(v1alpha1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Beta1(__sv_foo), "v1") => { + let converted: v1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1(v1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_foo), "v1alpha1") => { + let converted: v1beta1::FooSpec = __sv_foo.spec.into(); + let converted: v1alpha1::FooSpec = converted.into(); + let desired_object = Self::V1Alpha1(v1alpha1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_foo), "v1beta1") => { + let converted: v1beta1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1Beta1(v1beta1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + _ => converted_objects.push(object), + } + } + ::std::result::Result::Ok(converted_objects) + } + fn from_json_value( + value: ::serde_json::Value, + ) -> ::std::result::Result { + let api_version = value + .get("apiVersion") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + let object = match api_version { + "foo.example.org/v1alpha1" | "v1alpha1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Alpha1(object) + } + "foo.example.org/v1beta1" | "v1beta1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Beta1(object) + } + "foo.example.org/v1" | "v1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1(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)?), + Self::V1Beta1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + Self::V1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + } + } +} +#[automatically_derived] +pub enum FooVersion { + V1Alpha1, + V1Beta1, + V1, +} +#[automatically_derived] +impl ::std::fmt::Display for FooVersion { + fn fmt( + &self, + f: &mut ::std::fmt::Formatter<'_>, + ) -> ::std::result::Result<(), ::std::fmt::Error> { + f.write_str(self.as_str()) + } +} +#[automatically_derived] +impl FooVersion { + pub fn as_str(&self) -> &str { + match self { + V1Alpha1 => "v1alpha1", + V1Beta1 => "v1beta1", + V1 => "v1", + } + } } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module.rs.snap index 44fa16f86..d63333e70 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module.rs.snap @@ -277,28 +277,15 @@ pub(crate) mod v2alpha1 { } #[automatically_derived] pub(crate) enum Foo { - V1Alpha1, - V1, - V2Alpha1, -} -#[automatically_derived] -impl ::std::fmt::Display for Foo { - fn fmt( - &self, - f: &mut ::std::fmt::Formatter<'_>, - ) -> ::std::result::Result<(), ::std::fmt::Error> { - match self { - Self::V1Alpha1 => f.write_str("v1alpha1"), - Self::V1 => f.write_str("v1"), - Self::V2Alpha1 => f.write_str("v2alpha1"), - } - } + V1Alpha1(v1alpha1::Foo), + V1(v1::Foo), + V2Alpha1(v2alpha1::Foo), } #[automatically_derived] impl Foo { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( - stored_apiversion: Self, + stored_apiversion: FooVersion, ) -> ::std::result::Result< ::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, ::kube::core::crd::MergeError, @@ -309,34 +296,249 @@ impl Foo { as ::kube::core::CustomResourceExt > ::crd(), < v2alpha1::Foo as ::kube::core::CustomResourceExt > ::crd() ], - &stored_apiversion.to_string(), + stored_apiversion.as_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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: err.to_string(), + reason: err.to_string(), + details: None, + code: 400, + }) + .into_review(); + } + }; + let desired_api_version = request.desired_api_version.as_str(); + let response = match Self::convert_objects( + request.objects, + desired_api_version, + ) { + ::std::result::Result::Ok(converted_objects) => { + ::kube::core::conversion::ConversionResponse { + result: ::kube::client::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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: message.clone(), + 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 mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); + for object in objects { + let current_object = Self::from_json_value(object.clone()) + .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { + source, + })?; + match (current_object, desired_api_version) { + (Self::V1Alpha1(__sv_foo), "v1") => { + let converted: v1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1(v1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Alpha1(__sv_foo), "v2alpha1") => { + let converted: v1::FooSpec = __sv_foo.spec.into(); + let converted: v2alpha1::FooSpec = converted.into(); + let desired_object = Self::V2Alpha1(v2alpha1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_foo), "v1alpha1") => { + let converted: v1alpha1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1Alpha1(v1alpha1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_foo), "v2alpha1") => { + let converted: v2alpha1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V2Alpha1(v2alpha1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V2Alpha1(__sv_foo), "v1alpha1") => { + let converted: v1::FooSpec = __sv_foo.spec.into(); + let converted: v1alpha1::FooSpec = converted.into(); + let desired_object = Self::V1Alpha1(v1alpha1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V2Alpha1(__sv_foo), "v1") => { + let converted: v1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1(v1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + _ => converted_objects.push(object), + } + } + ::std::result::Result::Ok(converted_objects) + } + fn from_json_value( + value: ::serde_json::Value, + ) -> ::std::result::Result { + let api_version = value + .get("apiVersion") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + let object = match api_version { + "foo.example.org/v1alpha1" | "v1alpha1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Alpha1(object) + } + "foo.example.org/v1" | "v1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1(object) + } + "foo.example.org/v2alpha1" | "v2alpha1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V2Alpha1(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)?), + Self::V1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + Self::V2Alpha1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + } + } } #[automatically_derived] -pub(crate) enum Bar { +pub(crate) enum FooVersion { V1Alpha1, V1, V2Alpha1, } #[automatically_derived] -impl ::std::fmt::Display for Bar { +impl ::std::fmt::Display for FooVersion { fn fmt( &self, f: &mut ::std::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { + f.write_str(self.as_str()) + } +} +#[automatically_derived] +impl FooVersion { + pub fn as_str(&self) -> &str { match self { - Self::V1Alpha1 => f.write_str("v1alpha1"), - Self::V1 => f.write_str("v1"), - Self::V2Alpha1 => f.write_str("v2alpha1"), + V1Alpha1 => "v1alpha1", + V1 => "v1", + V2Alpha1 => "v2alpha1", } } } #[automatically_derived] +pub(crate) enum Bar { + V1Alpha1(v1alpha1::Bar), + V1(v1::Bar), + V2Alpha1(v2alpha1::Bar), +} +#[automatically_derived] impl Bar { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( - stored_apiversion: Self, + stored_apiversion: BarVersion, ) -> ::std::result::Result< ::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, ::kube::core::crd::MergeError, @@ -347,7 +549,235 @@ impl Bar { as ::kube::core::CustomResourceExt > ::crd(), < v2alpha1::Bar as ::kube::core::CustomResourceExt > ::crd() ], - &stored_apiversion.to_string(), + stored_apiversion.as_str(), ) } + ///Tries to convert a list of objects of kind [`Bar`] 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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: err.to_string(), + reason: err.to_string(), + details: None, + code: 400, + }) + .into_review(); + } + }; + let desired_api_version = request.desired_api_version.as_str(); + let response = match Self::convert_objects( + request.objects, + desired_api_version, + ) { + ::std::result::Result::Ok(converted_objects) => { + ::kube::core::conversion::ConversionResponse { + result: ::kube::client::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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: message.clone(), + 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 mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); + for object in objects { + let current_object = Self::from_json_value(object.clone()) + .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { + source, + })?; + match (current_object, desired_api_version) { + (Self::V1Alpha1(__sv_bar), "v1") => { + let converted: v1::BarSpec = __sv_bar.spec.into(); + let desired_object = Self::V1(v1::Bar { + metadata: __sv_bar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Alpha1(__sv_bar), "v2alpha1") => { + let converted: v1::BarSpec = __sv_bar.spec.into(); + let converted: v2alpha1::BarSpec = converted.into(); + let desired_object = Self::V2Alpha1(v2alpha1::Bar { + metadata: __sv_bar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_bar), "v1alpha1") => { + let converted: v1alpha1::BarSpec = __sv_bar.spec.into(); + let desired_object = Self::V1Alpha1(v1alpha1::Bar { + metadata: __sv_bar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_bar), "v2alpha1") => { + let converted: v2alpha1::BarSpec = __sv_bar.spec.into(); + let desired_object = Self::V2Alpha1(v2alpha1::Bar { + metadata: __sv_bar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V2Alpha1(__sv_bar), "v1alpha1") => { + let converted: v1::BarSpec = __sv_bar.spec.into(); + let converted: v1alpha1::BarSpec = converted.into(); + let desired_object = Self::V1Alpha1(v1alpha1::Bar { + metadata: __sv_bar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V2Alpha1(__sv_bar), "v1") => { + let converted: v1::BarSpec = __sv_bar.spec.into(); + let desired_object = Self::V1(v1::Bar { + metadata: __sv_bar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + _ => converted_objects.push(object), + } + } + ::std::result::Result::Ok(converted_objects) + } + fn from_json_value( + value: ::serde_json::Value, + ) -> ::std::result::Result { + let api_version = value + .get("apiVersion") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + let object = match api_version { + "bar.example.org/v1alpha1" | "v1alpha1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Alpha1(object) + } + "bar.example.org/v1" | "v1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1(object) + } + "bar.example.org/v2alpha1" | "v2alpha1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V2Alpha1(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_bar) => Ok(::serde_json::to_value(__sv_bar)?), + Self::V1(__sv_bar) => Ok(::serde_json::to_value(__sv_bar)?), + Self::V2Alpha1(__sv_bar) => Ok(::serde_json::to_value(__sv_bar)?), + } + } +} +#[automatically_derived] +pub(crate) enum BarVersion { + V1Alpha1, + V1, + V2Alpha1, +} +#[automatically_derived] +impl ::std::fmt::Display for BarVersion { + fn fmt( + &self, + f: &mut ::std::fmt::Formatter<'_>, + ) -> ::std::result::Result<(), ::std::fmt::Error> { + f.write_str(self.as_str()) + } +} +#[automatically_derived] +impl BarVersion { + pub fn as_str(&self) -> &str { + match self { + V1Alpha1 => "v1alpha1", + V1 => "v1", + V2Alpha1 => "v2alpha1", + } + } } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module_preserve.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module_preserve.rs.snap index 5fe460b51..6a74e8b0c 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module_preserve.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module_preserve.rs.snap @@ -259,26 +259,14 @@ pub(crate) mod versioned { } } pub enum Foo { - V1Alpha1, - V1, - V2Alpha1, - } - impl ::std::fmt::Display for Foo { - fn fmt( - &self, - f: &mut ::std::fmt::Formatter<'_>, - ) -> ::std::result::Result<(), ::std::fmt::Error> { - match self { - Self::V1Alpha1 => f.write_str("v1alpha1"), - Self::V1 => f.write_str("v1"), - Self::V2Alpha1 => f.write_str("v2alpha1"), - } - } + V1Alpha1(v1alpha1::Foo), + V1(v1::Foo), + V2Alpha1(v2alpha1::Foo), } impl Foo { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( - stored_apiversion: Self, + stored_apiversion: FooVersion, ) -> ::std::result::Result< ::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, ::kube::core::crd::MergeError, @@ -289,31 +277,244 @@ pub(crate) mod versioned { v1::Foo as ::kube::core::CustomResourceExt > ::crd(), < v2alpha1::Foo as ::kube::core::CustomResourceExt > ::crd() ], - &stored_apiversion.to_string(), + stored_apiversion.as_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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: err.to_string(), + reason: err.to_string(), + details: None, + code: 400, + }) + .into_review(); + } + }; + let desired_api_version = request.desired_api_version.as_str(); + let response = match Self::convert_objects( + request.objects, + desired_api_version, + ) { + ::std::result::Result::Ok(converted_objects) => { + ::kube::core::conversion::ConversionResponse { + result: ::kube::client::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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: message.clone(), + 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 mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); + for object in objects { + let current_object = Self::from_json_value(object.clone()) + .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { + source, + })?; + match (current_object, desired_api_version) { + (Self::V1Alpha1(__sv_foo), "v1") => { + let converted: v1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1(v1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Alpha1(__sv_foo), "v2alpha1") => { + let converted: v1::FooSpec = __sv_foo.spec.into(); + let converted: v2alpha1::FooSpec = converted.into(); + let desired_object = Self::V2Alpha1(v2alpha1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_foo), "v1alpha1") => { + let converted: v1alpha1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1Alpha1(v1alpha1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_foo), "v2alpha1") => { + let converted: v2alpha1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V2Alpha1(v2alpha1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V2Alpha1(__sv_foo), "v1alpha1") => { + let converted: v1::FooSpec = __sv_foo.spec.into(); + let converted: v1alpha1::FooSpec = converted.into(); + let desired_object = Self::V1Alpha1(v1alpha1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V2Alpha1(__sv_foo), "v1") => { + let converted: v1::FooSpec = __sv_foo.spec.into(); + let desired_object = Self::V1(v1::Foo { + metadata: __sv_foo.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + _ => converted_objects.push(object), + } + } + ::std::result::Result::Ok(converted_objects) + } + fn from_json_value( + value: ::serde_json::Value, + ) -> ::std::result::Result { + let api_version = value + .get("apiVersion") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + let object = match api_version { + "foo.example.org/v1alpha1" | "v1alpha1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Alpha1(object) + } + "foo.example.org/v1" | "v1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1(object) + } + "foo.example.org/v2alpha1" | "v2alpha1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V2Alpha1(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)?), + Self::V1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + Self::V2Alpha1(__sv_foo) => Ok(::serde_json::to_value(__sv_foo)?), + } + } } - pub enum Bar { + pub enum FooVersion { V1Alpha1, V1, V2Alpha1, } - impl ::std::fmt::Display for Bar { + impl ::std::fmt::Display for FooVersion { fn fmt( &self, f: &mut ::std::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { + f.write_str(self.as_str()) + } + } + impl FooVersion { + pub fn as_str(&self) -> &str { match self { - Self::V1Alpha1 => f.write_str("v1alpha1"), - Self::V1 => f.write_str("v1"), - Self::V2Alpha1 => f.write_str("v2alpha1"), + V1Alpha1 => "v1alpha1", + V1 => "v1", + V2Alpha1 => "v2alpha1", } } } + pub enum Bar { + V1Alpha1(v1alpha1::Bar), + V1(v1::Bar), + V2Alpha1(v2alpha1::Bar), + } impl Bar { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( - stored_apiversion: Self, + stored_apiversion: BarVersion, ) -> ::std::result::Result< ::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, ::kube::core::crd::MergeError, @@ -324,8 +525,233 @@ pub(crate) mod versioned { v1::Bar as ::kube::core::CustomResourceExt > ::crd(), < v2alpha1::Bar as ::kube::core::CustomResourceExt > ::crd() ], - &stored_apiversion.to_string(), + stored_apiversion.as_str(), ) } + ///Tries to convert a list of objects of kind [`Bar`] 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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: err.to_string(), + reason: err.to_string(), + details: None, + code: 400, + }) + .into_review(); + } + }; + let desired_api_version = request.desired_api_version.as_str(); + let response = match Self::convert_objects( + request.objects, + desired_api_version, + ) { + ::std::result::Result::Ok(converted_objects) => { + ::kube::core::conversion::ConversionResponse { + result: ::kube::client::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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: message.clone(), + 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 mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); + for object in objects { + let current_object = Self::from_json_value(object.clone()) + .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { + source, + })?; + match (current_object, desired_api_version) { + (Self::V1Alpha1(__sv_bar), "v1") => { + let converted: v1::BarSpec = __sv_bar.spec.into(); + let desired_object = Self::V1(v1::Bar { + metadata: __sv_bar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Alpha1(__sv_bar), "v2alpha1") => { + let converted: v1::BarSpec = __sv_bar.spec.into(); + let converted: v2alpha1::BarSpec = converted.into(); + let desired_object = Self::V2Alpha1(v2alpha1::Bar { + metadata: __sv_bar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_bar), "v1alpha1") => { + let converted: v1alpha1::BarSpec = __sv_bar.spec.into(); + let desired_object = Self::V1Alpha1(v1alpha1::Bar { + metadata: __sv_bar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_bar), "v2alpha1") => { + let converted: v2alpha1::BarSpec = __sv_bar.spec.into(); + let desired_object = Self::V2Alpha1(v2alpha1::Bar { + metadata: __sv_bar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V2Alpha1(__sv_bar), "v1alpha1") => { + let converted: v1::BarSpec = __sv_bar.spec.into(); + let converted: v1alpha1::BarSpec = converted.into(); + let desired_object = Self::V1Alpha1(v1alpha1::Bar { + metadata: __sv_bar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V2Alpha1(__sv_bar), "v1") => { + let converted: v1::BarSpec = __sv_bar.spec.into(); + let desired_object = Self::V1(v1::Bar { + metadata: __sv_bar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + _ => converted_objects.push(object), + } + } + ::std::result::Result::Ok(converted_objects) + } + fn from_json_value( + value: ::serde_json::Value, + ) -> ::std::result::Result { + let api_version = value + .get("apiVersion") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + let object = match api_version { + "bar.example.org/v1alpha1" | "v1alpha1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Alpha1(object) + } + "bar.example.org/v1" | "v1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1(object) + } + "bar.example.org/v2alpha1" | "v2alpha1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V2Alpha1(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_bar) => Ok(::serde_json::to_value(__sv_bar)?), + Self::V1(__sv_bar) => Ok(::serde_json::to_value(__sv_bar)?), + Self::V2Alpha1(__sv_bar) => Ok(::serde_json::to_value(__sv_bar)?), + } + } + } + pub enum BarVersion { + V1Alpha1, + V1, + V2Alpha1, + } + impl ::std::fmt::Display for BarVersion { + fn fmt( + &self, + f: &mut ::std::fmt::Formatter<'_>, + ) -> ::std::result::Result<(), ::std::fmt::Error> { + f.write_str(self.as_str()) + } + } + impl BarVersion { + pub fn as_str(&self) -> &str { + match self { + V1Alpha1 => "v1alpha1", + V1 => "v1", + V2Alpha1 => "v2alpha1", + } + } } } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@renamed_kind.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@renamed_kind.rs.snap index ea38693a9..d284f43c1 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@renamed_kind.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@renamed_kind.rs.snap @@ -90,28 +90,15 @@ pub mod v1 { } #[automatically_derived] pub enum FooBar { - V1Alpha1, - V1Beta1, - V1, -} -#[automatically_derived] -impl ::std::fmt::Display for FooBar { - fn fmt( - &self, - f: &mut ::std::fmt::Formatter<'_>, - ) -> ::std::result::Result<(), ::std::fmt::Error> { - match self { - Self::V1Alpha1 => f.write_str("v1alpha1"), - Self::V1Beta1 => f.write_str("v1beta1"), - Self::V1 => f.write_str("v1"), - } - } + V1Alpha1(v1alpha1::FooBar), + V1Beta1(v1beta1::FooBar), + V1(v1::FooBar), } #[automatically_derived] impl FooBar { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( - stored_apiversion: Self, + stored_apiversion: FooBarVersion, ) -> ::std::result::Result< ::k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, ::kube::core::crd::MergeError, @@ -122,7 +109,235 @@ impl FooBar { v1beta1::FooBar as ::kube::core::CustomResourceExt > ::crd(), < v1::FooBar as ::kube::core::CustomResourceExt > ::crd() ], - &stored_apiversion.to_string(), + stored_apiversion.as_str(), ) } + ///Tries to convert a list of objects of kind [`FooBar`] 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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: err.to_string(), + reason: err.to_string(), + details: None, + code: 400, + }) + .into_review(); + } + }; + let desired_api_version = request.desired_api_version.as_str(); + let response = match Self::convert_objects( + request.objects, + desired_api_version, + ) { + ::std::result::Result::Ok(converted_objects) => { + ::kube::core::conversion::ConversionResponse { + result: ::kube::client::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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: message.clone(), + 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 mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); + for object in objects { + let current_object = Self::from_json_value(object.clone()) + .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { + source, + })?; + match (current_object, desired_api_version) { + (Self::V1Alpha1(__sv_foobar), "v1beta1") => { + let converted: v1beta1::FooSpec = __sv_foobar.spec.into(); + let desired_object = Self::V1Beta1(v1beta1::FooBar { + metadata: __sv_foobar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Alpha1(__sv_foobar), "v1") => { + let converted: v1beta1::FooSpec = __sv_foobar.spec.into(); + let converted: v1::FooSpec = converted.into(); + let desired_object = Self::V1(v1::FooBar { + metadata: __sv_foobar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Beta1(__sv_foobar), "v1alpha1") => { + let converted: v1alpha1::FooSpec = __sv_foobar.spec.into(); + let desired_object = Self::V1Alpha1(v1alpha1::FooBar { + metadata: __sv_foobar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1Beta1(__sv_foobar), "v1") => { + let converted: v1::FooSpec = __sv_foobar.spec.into(); + let desired_object = Self::V1(v1::FooBar { + metadata: __sv_foobar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_foobar), "v1alpha1") => { + let converted: v1beta1::FooSpec = __sv_foobar.spec.into(); + let converted: v1alpha1::FooSpec = converted.into(); + let desired_object = Self::V1Alpha1(v1alpha1::FooBar { + metadata: __sv_foobar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + (Self::V1(__sv_foobar), "v1beta1") => { + let converted: v1beta1::FooSpec = __sv_foobar.spec.into(); + let desired_object = Self::V1Beta1(v1beta1::FooBar { + metadata: __sv_foobar.metadata, + spec: converted, + }); + let desired_object = desired_object + .into_json_value() + .map_err(|source| ::stackable_versioned::ConvertObjectError::Serialize { + source, + })?; + converted_objects.push(desired_object); + } + _ => converted_objects.push(object), + } + } + ::std::result::Result::Ok(converted_objects) + } + fn from_json_value( + value: ::serde_json::Value, + ) -> ::std::result::Result { + let api_version = value + .get("apiVersion") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + let object = match api_version { + "stackable.tech/v1alpha1" | "v1alpha1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Alpha1(object) + } + "stackable.tech/v1beta1" | "v1beta1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1Beta1(object) + } + "stackable.tech/v1" | "v1" => { + let object = ::serde_json::from_value(value) + .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { + source, + })?; + Self::V1(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_foobar) => Ok(::serde_json::to_value(__sv_foobar)?), + Self::V1Beta1(__sv_foobar) => Ok(::serde_json::to_value(__sv_foobar)?), + Self::V1(__sv_foobar) => Ok(::serde_json::to_value(__sv_foobar)?), + } + } +} +#[automatically_derived] +pub enum FooBarVersion { + V1Alpha1, + V1Beta1, + V1, +} +#[automatically_derived] +impl ::std::fmt::Display for FooBarVersion { + fn fmt( + &self, + f: &mut ::std::fmt::Formatter<'_>, + ) -> ::std::result::Result<(), ::std::fmt::Error> { + f.write_str(self.as_str()) + } +} +#[automatically_derived] +impl FooBarVersion { + pub fn as_str(&self) -> &str { + match self { + V1Alpha1 => "v1alpha1", + V1Beta1 => "v1beta1", + V1 => "v1", + } + } } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@shortnames.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@shortnames.rs.snap index 582d1db4e..94e14e24d 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@shortnames.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@shortnames.rs.snap @@ -25,31 +25,148 @@ pub(crate) mod v1alpha1 { } #[automatically_derived] pub(crate) enum Foo { - V1Alpha1, -} -#[automatically_derived] -impl ::std::fmt::Display for Foo { - fn fmt( - &self, - f: &mut ::std::fmt::Formatter<'_>, - ) -> ::std::result::Result<(), ::std::fmt::Error> { - match self { - Self::V1Alpha1 => f.write_str("v1alpha1"), - } - } + 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: Self, + 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.to_string(), + stored_apiversion.as_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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: err.to_string(), + reason: err.to_string(), + details: None, + code: 400, + }) + .into_review(); + } + }; + let desired_api_version = request.desired_api_version.as_str(); + let response = match Self::convert_objects( + request.objects, + desired_api_version, + ) { + ::std::result::Result::Ok(converted_objects) => { + ::kube::core::conversion::ConversionResponse { + result: ::kube::client::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::client::Status { + status: Some(::kube::core::response::StatusSummary::Failure), + message: message.clone(), + 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 mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); + for object in objects { + let current_object = Self::from_json_value(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_value( + value: ::serde_json::Value, + ) -> ::std::result::Result { + let api_version = value + .get("apiVersion") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + let object = match api_version { + "stackable.tech/v1alpha1" | "v1alpha1" => { + let object = ::serde_json::from_value(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] +pub(crate) enum FooVersion { + V1Alpha1, +} +#[automatically_derived] +impl ::std::fmt::Display for FooVersion { + fn fmt( + &self, + f: &mut ::std::fmt::Formatter<'_>, + ) -> ::std::result::Result<(), ::std::fmt::Error> { + f.write_str(self.as_str()) + } +} +#[automatically_derived] +impl FooVersion { + pub fn as_str(&self) -> &str { + match self { + V1Alpha1 => "v1alpha1", + } + } } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@skip.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@skip.rs.snap deleted file mode 100644 index 84bcd909b..000000000 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@skip.rs.snap +++ /dev/null @@ -1,90 +0,0 @@ ---- -source: crates/stackable-versioned-macros/src/lib.rs -expression: formatted -input_file: crates/stackable-versioned-macros/tests/inputs/k8s/pass/skip.rs ---- -#[automatically_derived] -pub mod v1alpha1 { - use super::*; - #[derive( - Clone, - Debug, - serde::Deserialize, - serde::Serialize, - schemars::JsonSchema, - kube::CustomResource, - )] - #[kube(group = "stackable.tech", version = "v1alpha1", kind = "Foo")] - pub struct FooSpec { - pub baz: bool, - } -} -#[automatically_derived] -impl ::std::convert::From for v1beta1::FooSpec { - fn from(__sv_foospec: v1alpha1::FooSpec) -> Self { - Self { - bah: ::std::default::Default::default(), - baz: __sv_foospec.baz.into(), - } - } -} -#[automatically_derived] -impl ::std::convert::From for v1alpha1::FooSpec { - fn from(__sv_foospec: v1beta1::FooSpec) -> Self { - Self { - baz: __sv_foospec.baz.into(), - } - } -} -#[automatically_derived] -pub mod v1beta1 { - use super::*; - #[derive( - Clone, - Debug, - serde::Deserialize, - serde::Serialize, - schemars::JsonSchema, - kube::CustomResource, - )] - #[kube(group = "stackable.tech", version = "v1beta1", kind = "Foo")] - pub struct FooSpec { - pub bah: usize, - pub baz: bool, - } -} -#[automatically_derived] -impl ::std::convert::From for v1::FooSpec { - fn from(__sv_foospec: v1beta1::FooSpec) -> Self { - Self { - bar: __sv_foospec.bah.into(), - baz: __sv_foospec.baz.into(), - } - } -} -#[automatically_derived] -impl ::std::convert::From for v1beta1::FooSpec { - fn from(__sv_foospec: v1::FooSpec) -> Self { - Self { - bah: __sv_foospec.bar.into(), - baz: __sv_foospec.baz.into(), - } - } -} -#[automatically_derived] -pub mod v1 { - use super::*; - #[derive( - Clone, - Debug, - serde::Deserialize, - serde::Serialize, - schemars::JsonSchema, - kube::CustomResource, - )] - #[kube(group = "stackable.tech", version = "v1", kind = "Foo")] - pub struct FooSpec { - pub bar: usize, - pub baz: bool, - } -} diff --git a/crates/stackable-versioned/CHANGELOG.md b/crates/stackable-versioned/CHANGELOG.md index 07238274a..524169ad3 100644 --- a/crates/stackable-versioned/CHANGELOG.md +++ b/crates/stackable-versioned/CHANGELOG.md @@ -6,6 +6,9 @@ All notable changes to this project will be documented in this file. ### Added +- Add support for CRD conversions via ConversionReviews ([#1050]). + - Add new `try_convert` function to convert objects received via a ConversionReview. + - Add new `enable_tracing` option to `#[versioned(k8s(options(...)))]`. - Implement basic ground work for downgrading custom resources ([#1033]). - Emit `From` implementations to downgrade custom resource specs. - Emit a status struct to be able to track values required during downgrades and upgrades of @@ -20,6 +23,7 @@ All notable changes to this project will be documented in this file. ### Changed +- BREAKING: The version enum used in `merged_crd` is now suffixed with `Version` ([#1050]). - BREAKING: The `convert_with` parameter of the `changed()` action was renamed and split into two parts to be able to control the conversion during upgrades and downgrades: `upgrade_with` and `downgrade_with` ([#1033]). @@ -32,6 +36,7 @@ All notable changes to this project will be documented in this file. ### Removed +- BREAKING: The `#[versioned(k8s(skip(merged_crd)))]` flag has been removed ([#1050]). - BREAKING: Remove unused `AsVersionStr` trait ([#1033]). ### Miscellaneous @@ -44,6 +49,7 @@ All notable changes to this project will be documented in this file. [#1038]: https://github.com/stackabletech/operator-rs/pull/1038 [#1041]: https://github.com/stackabletech/operator-rs/pull/1041 [#1046]: https://github.com/stackabletech/operator-rs/pull/1046 +[#1050]: https://github.com/stackabletech/operator-rs/pull/1050 ## [0.7.1] - 2025-04-02 diff --git a/crates/stackable-versioned/Cargo.toml b/crates/stackable-versioned/Cargo.toml index b5dc5218b..f25cc40db 100644 --- a/crates/stackable-versioned/Cargo.toml +++ b/crates/stackable-versioned/Cargo.toml @@ -15,16 +15,19 @@ full = ["k8s"] k8s = [ "stackable-versioned-macros/k8s", # Forward the k8s feature to the underlying macro crate "dep:k8s-version", + "dep:schemars", "dep:serde_json", "dep:serde_yaml", - "dep:schemars", "dep:serde", + "dep:snafu", ] [dependencies] k8s-version = { path = "../k8s-version", features = ["serde"], optional = true } stackable-versioned-macros = { path = "../stackable-versioned-macros" } + schemars = { workspace = true, optional = true } serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } serde_yaml = { workspace = true, optional = true } +snafu = { workspace = true, optional = true } diff --git a/crates/stackable-versioned/src/k8s.rs b/crates/stackable-versioned/src/k8s.rs index b0cda13dc..63dd01ade 100644 --- a/crates/stackable-versioned/src/k8s.rs +++ b/crates/stackable-versioned/src/k8s.rs @@ -2,11 +2,13 @@ use std::collections::HashMap; use k8s_version::Version; use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec}; +use snafu::{ErrorCompat, Snafu}; // NOTE (@Techassi): This struct represents a rough first draft of how tracking values across // CRD versions can be achieved. It is currently untested and unproven and might change down the // line. Currently, this struct is only generated by the macro but not actually used by any other // code. The tracking itself will be introduced in a follow-up PR. +/// Contains changed values during upgrades and downgrades of CRDs. #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] pub struct ChangedValues { /// List of values needed when downgrading to a particular version. @@ -16,6 +18,7 @@ pub struct ChangedValues { pub upgrades: HashMap>, } +/// Contains a changed value for a single field of the CRD. #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] pub struct ChangedValue { /// The name of the field of the custom resource this value is for. @@ -40,3 +43,52 @@ fn raw_object_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema { ..Default::default() }) } + +/// This error indicates that parsing an object from a conversion review failed. +#[derive(Debug, Snafu)] +pub enum ParseObjectError { + #[snafu(display(r#"failed to find "apiVersion" field"#))] + FieldNotPresent, + + #[snafu(display(r#"the "apiVersion" field must be a string"#))] + FieldNotStr, + + #[snafu(display("encountered unknown object API version {api_version:?}"))] + UnknownApiVersion { api_version: String }, + + #[snafu(display("failed to deserialize object from JSON"))] + Deserialize { source: serde_json::Error }, +} + +/// This error indicates that converting an object from a conversion review to the desired +/// version failed. +#[derive(Debug, Snafu)] +pub enum ConvertObjectError { + #[snafu(display("failed to parse object"))] + Parse { source: ParseObjectError }, + + #[snafu(display("failed to serialize object into json"))] + Serialize { source: serde_json::Error }, +} + +impl ConvertObjectError { + /// Joins the error and its sources using colons. + pub fn join_errors(&self) -> String { + // NOTE (@Techassi): This can be done with itertools in a way shorter + // fashion but obviously brings in another dependency. Which of those + // two solutions performs better needs to evaluated. + // self.iter_chain().join(": ") + self.iter_chain() + .map(|err| err.to_string()) + .collect::>() + .join(": ") + } + + /// Returns a HTTP status code based on the underlying error. + pub fn http_status_code(&self) -> u16 { + match self { + ConvertObjectError::Parse { .. } => 400, + ConvertObjectError::Serialize { .. } => 500, + } + } +} diff --git a/crates/stackable-versioned/src/lib.rs b/crates/stackable-versioned/src/lib.rs index 85edf234e..5538eae65 100644 --- a/crates/stackable-versioned/src/lib.rs +++ b/crates/stackable-versioned/src/lib.rs @@ -1,21 +1,19 @@ -//! This crate enables versioning of structs and enums through procedural -//! macros. +//! This crate enables versioning of structs and enums through procedural macros. //! //! Currently supported versioning schemes: //! -//! - Kubernetes API versions (eg: `v1alpha1`, `v1beta1`, `v1`, `v2`), with -//! optional support for generating CRDs. +//! - Kubernetes API versions (eg: `v1alpha1`, `v1beta1`, `v1`, `v2`), with optional support for +//! generating CRDs. //! -//! Support will be extended to SemVer versions, as well as custom version -//! formats in the future. +//! Support will be extended to SemVer versions, as well as custom version formats in the future. //! -//! See [`versioned`] for an in-depth usage guide and a list of supported -//! parameters. +//! See [`versioned`] for an in-depth usage guide and a list of supported arguments. -// Re-export macro -#[cfg(feature = "k8s")] -pub use k8s::*; +// Re-exports pub use stackable_versioned_macros::versioned; +// Behind k8s feature #[cfg(feature = "k8s")] mod k8s; +#[cfg(feature = "k8s")] +pub use k8s::*; diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml index bb56c9a18..56aab6aea 100644 --- a/crates/xtask/Cargo.toml +++ b/crates/xtask/Cargo.toml @@ -7,6 +7,7 @@ publish = false stackable-operator = { path = "../stackable-operator" } clap.workspace = true +paste.workspace = true serde.workspace = true serde_json.workspace = true snafu.workspace = true diff --git a/crates/xtask/src/crd/dummy.rs b/crates/xtask/src/crd/dummy.rs index a3a9e291f..6c5fd7871 100644 --- a/crates/xtask/src/crd/dummy.rs +++ b/crates/xtask/src/crd/dummy.rs @@ -18,8 +18,10 @@ pub mod versioned { namespaced, crates( kube_core = "stackable_operator::kube::core", + kube_client = "stackable_operator::kube::client", k8s_openapi = "stackable_operator::k8s_openapi", - schemars = "stackable_operator::schemars" + schemars = "stackable_operator::schemars", + versioned = "stackable_operator::versioned" ) ))] #[derive(Clone, CustomResource, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] diff --git a/crates/xtask/src/crd/mod.rs b/crates/xtask/src/crd/mod.rs index 6e4ef98ef..0b8ca9282 100644 --- a/crates/xtask/src/crd/mod.rs +++ b/crates/xtask/src/crd/mod.rs @@ -1,16 +1,20 @@ use std::path::PathBuf; +use paste::paste; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ crd::{ - authentication::core::AuthenticationClass, - listener::{Listener, ListenerClass, PodListeners}, - s3::{S3Bucket, S3Connection}, + authentication::core::{AuthenticationClass, AuthenticationClassVersion}, + listener::{ + Listener, ListenerClass, ListenerClassVersion, ListenerVersion, PodListeners, + PodListenersVersion, + }, + s3::{S3Bucket, S3BucketVersion, S3Connection, S3ConnectionVersion}, }, kube::core::crd::MergeError, }; -use crate::crd::dummy::DummyCluster; +use crate::crd::dummy::{DummyCluster, DummyClusterVersion}; mod dummy; @@ -37,21 +41,23 @@ pub enum Error { macro_rules! write_crd { ($base_path:expr, $crd_name:ident, $stored_crd_version:ident) => { - let merged = $crd_name::merged_crd($crd_name::$stored_crd_version) - .context(MergeCrdSnafu { crd_name: stringify!($crd_name) })?; + paste! { + let merged = $crd_name::merged_crd([<$crd_name Version>]::$stored_crd_version) + .context(MergeCrdSnafu { crd_name: stringify!($crd_name) })?; - let mut path = $base_path.join(stringify!($crd_name)); - path.set_extension("yaml"); + let mut path = $base_path.join(stringify!($crd_name)); + path.set_extension("yaml"); - - ::write_yaml_schema( - &merged, - &path, - "0.0.0-dev", - stackable_operator::shared::yaml::SerializeOptions::default(), - ) - .with_context(|_| WriteCrdSnafu { path: path.clone() })?; + + ::write_yaml_schema( + &merged, + &path, + "0.0.0-dev", + stackable_operator::shared::yaml::SerializeOptions::default(), + ) + .with_context(|_| WriteCrdSnafu { path: path.clone() })?; + } }; } diff --git a/deny.toml b/deny.toml index 6e83b372c..6e7cfa7cd 100644 --- a/deny.toml +++ b/deny.toml @@ -13,13 +13,26 @@ ignore = [ # https://rustsec.org/advisories/RUSTSEC-2023-0071 # "rsa" crate: Marvin Attack: potential key recovery through timing sidechannel # - # No patch is yet available, however work is underway to migrate to a fully constant-time implementation - # So we need to accept this, as of SDP 24.11 we are not using the rsa crate to create certificates used in production - # setups. + # No patch is yet available, however work is underway to migrate to a fully constant-time + # implementation. So we need to accept this, as of SDP 24.11 we are not using the rsa crate to + # create certificates used in production setups. # # TODO: Remove after https://github.com/RustCrypto/RSA/pull/394 is merged and v0.10.0 is released "RUSTSEC-2023-0071", + # https://rustsec.org/advisories/RUSTSEC-2024-0436 + # The "paste" crate is no longer maintained because the owner states that the implementation is + # finished. There are at least two (forked) alternatives which state to be maintained. They'd + # need to be vetted before a potential switch. Additionally, they'd need to be in a maintained + # state for a couple of years to provide any benefit over using "paste". + # + # This crate is only used in a single place in the xtask package inside the declarative + # "write_crd" macro. The impact of vulnerabilities, if any, should be fairly minimal. + # + # See thread: https://users.rust-lang.org/t/paste-alternatives/126787/4 + # + # This can only be removed again if we decide to use a different crate. + "RUSTSEC-2024-0436", ] [bans] @@ -38,7 +51,10 @@ allow = [ "LicenseRef-webpki", "MIT", "MPL-2.0", - "OpenSSL", # Needed for the ring and/or aws-lc-sys crate. See https://github.com/stackabletech/operator-templating/pull/464 for details + + # Needed for the ring and/or aws-lc-sys crate. + # See https://github.com/stackabletech/operator-templating/pull/464 for details. + "OpenSSL", "Unicode-3.0", "Unicode-DFS-2016", "Zlib",