diff --git a/.vscode/settings.json b/.vscode/settings.json index 8c9fdee9f..2571fe27f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,8 @@ "rust-analyzer.rustfmt.overrideCommand": [ "rustfmt", "+nightly-2025-05-26", + "--edition", + "2024", "--" ] } diff --git a/crates/stackable-operator/Cargo.toml b/crates/stackable-operator/Cargo.toml index af065061f..b15574e6e 100644 --- a/crates/stackable-operator/Cargo.toml +++ b/crates/stackable-operator/Cargo.toml @@ -16,8 +16,8 @@ versioned = [] [dependencies] stackable-telemetry = { path = "../stackable-telemetry", features = ["clap"] } -stackable-versioned = { path = "../stackable-versioned", features = ["k8s"] } stackable-operator-derive = { path = "../stackable-operator-derive" } +stackable-versioned = { path = "../stackable-versioned" } stackable-shared = { path = "../stackable-shared" } chrono.workspace = true diff --git a/crates/stackable-operator/src/crd/authentication/core/mod.rs b/crates/stackable-operator/src/crd/authentication/core/mod.rs index 9287072e3..f466faf5f 100644 --- a/crates/stackable-operator/src/crd/authentication/core/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/core/mod.rs @@ -6,7 +6,14 @@ use crate::versioned::versioned; mod v1alpha1_impl; -#[versioned(version(name = "v1alpha1"))] +#[versioned( + version(name = "v1alpha1"), + crates( + kube_core = "kube::core", + k8s_openapi = "k8s_openapi", + schemars = "schemars", + ) +)] pub mod versioned { pub mod v1alpha1 { // Re-export the v1alpha1-specific error type from the private impl module. @@ -26,14 +33,9 @@ pub mod versioned { /// /// [1]: DOCS_BASE_URL_PLACEHOLDER/concepts/authentication /// [2]: DOCS_BASE_URL_PLACEHOLDER/tutorials/authentication_with_openldap - #[versioned(k8s( + #[versioned(crd( group = "authentication.stackable.tech", plural = "authenticationclasses", - crates( - kube_core = "kube::core", - k8s_openapi = "k8s_openapi", - schemars = "schemars", - ) ))] #[derive( Clone, diff --git a/crates/stackable-operator/src/crd/authentication/kerberos/mod.rs b/crates/stackable-operator/src/crd/authentication/kerberos/mod.rs index afff0b374..ccb2d4396 100644 --- a/crates/stackable-operator/src/crd/authentication/kerberos/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/kerberos/mod.rs @@ -3,11 +3,13 @@ use serde::{Deserialize, Serialize}; use stackable_versioned::versioned; #[versioned(version(name = "v1alpha1"))] -#[derive( - Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, -)] -#[serde(rename_all = "camelCase")] -pub struct AuthenticationProvider { - /// Mandatory SecretClass used to obtain keytabs. - pub kerberos_secret_class: String, +pub mod versioned { + #[derive( + Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, + )] + #[serde(rename_all = "camelCase")] + pub struct AuthenticationProvider { + /// Mandatory SecretClass used to obtain keytabs. + pub kerberos_secret_class: String, + } } diff --git a/crates/stackable-operator/src/crd/authentication/tls/mod.rs b/crates/stackable-operator/src/crd/authentication/tls/mod.rs index 38bdcb633..cf44a145f 100644 --- a/crates/stackable-operator/src/crd/authentication/tls/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/tls/mod.rs @@ -3,14 +3,16 @@ use serde::{Deserialize, Serialize}; use stackable_versioned::versioned; #[versioned(version(name = "v1alpha1"))] -#[derive( - Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, -)] -#[serde(rename_all = "camelCase")] -pub struct AuthenticationProvider { - /// See [ADR017: TLS authentication](DOCS_BASE_URL_PLACEHOLDER/contributor/adr/adr017-tls_authentication). - /// If `client_cert_secret_class` is not set, the TLS settings may also be used for client authentication. - /// If `client_cert_secret_class` is set, the [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass) - /// will be used to provision client certificates. - pub client_cert_secret_class: Option, +pub mod versioned { + #[derive( + Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, + )] + #[serde(rename_all = "camelCase")] + pub struct AuthenticationProvider { + /// See [ADR017: TLS authentication](DOCS_BASE_URL_PLACEHOLDER/contributor/adr/adr017-tls_authentication). + /// If `client_cert_secret_class` is not set, the TLS settings may also be used for client authentication. + /// If `client_cert_secret_class` is set, the [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass) + /// will be used to provision client certificates. + pub client_cert_secret_class: Option, + } } diff --git a/crates/stackable-operator/src/crd/listener/class/mod.rs b/crates/stackable-operator/src/crd/listener/class/mod.rs index ecbd50109..25054e26f 100644 --- a/crates/stackable-operator/src/crd/listener/class/mod.rs +++ b/crates/stackable-operator/src/crd/listener/class/mod.rs @@ -25,7 +25,7 @@ pub mod versioned { /// Defines a policy for how [Listeners](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener) should be exposed. /// Read the [ListenerClass documentation](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass) /// for more information. - #[versioned(k8s(group = "listeners.stackable.tech"))] + #[versioned(crd(group = "listeners.stackable.tech"))] #[derive(CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ListenerClassSpec { diff --git a/crates/stackable-operator/src/crd/listener/listeners/mod.rs b/crates/stackable-operator/src/crd/listener/listeners/mod.rs index 095eeb9d0..1d2fdf93a 100644 --- a/crates/stackable-operator/src/crd/listener/listeners/mod.rs +++ b/crates/stackable-operator/src/crd/listener/listeners/mod.rs @@ -49,7 +49,7 @@ pub mod versioned { /// ["sticky" scheduling](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener#_sticky_scheduling). /// /// Learn more in the [Listener documentation](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener). - #[versioned(k8s( + #[versioned(crd( group = "listeners.stackable.tech", status = "v1alpha1::ListenerStatus", namespaced @@ -79,7 +79,7 @@ pub mod versioned { /// This is not expected to be created or modified by users. It will be created by /// the Stackable Listener Operator when mounting the listener volume, and is always /// named `pod-{pod.metadata.uid}`. - #[versioned(k8s( + #[versioned(crd( group = "listeners.stackable.tech", plural = "podlisteners", namespaced, diff --git a/crates/stackable-operator/src/crd/s3/bucket/mod.rs b/crates/stackable-operator/src/crd/s3/bucket/mod.rs index 335835c84..9ec56d2a8 100644 --- a/crates/stackable-operator/src/crd/s3/bucket/mod.rs +++ b/crates/stackable-operator/src/crd/s3/bucket/mod.rs @@ -6,7 +6,14 @@ use crate::{crd::s3::connection::v1alpha1 as conn_v1alpha1, versioned::versioned mod v1alpha1_impl; -#[versioned(version(name = "v1alpha1"))] +#[versioned( + version(name = "v1alpha1"), + crates( + kube_core = "kube::core", + k8s_openapi = "k8s_openapi", + schemars = "schemars", + ) +)] pub mod versioned { pub mod v1alpha1 { pub use v1alpha1_impl::BucketError; @@ -14,15 +21,10 @@ pub mod versioned { /// S3 bucket specification containing the bucket name and an inlined or referenced connection specification. /// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3). - #[versioned(k8s( + #[versioned(crd( group = "s3.stackable.tech", kind = "S3Bucket", plural = "s3buckets", - crates( - kube_core = "kube::core", - k8s_openapi = "k8s_openapi", - schemars = "schemars", - ), namespaced ))] #[derive(Clone, CustomResource, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] diff --git a/crates/stackable-operator/src/crd/s3/connection/mod.rs b/crates/stackable-operator/src/crd/s3/connection/mod.rs index 97c754e2c..b41c280d8 100644 --- a/crates/stackable-operator/src/crd/s3/connection/mod.rs +++ b/crates/stackable-operator/src/crd/s3/connection/mod.rs @@ -16,7 +16,14 @@ mod v1alpha1_impl; /// Use this type in you operator! pub type ResolvedConnection = v1alpha1::ConnectionSpec; -#[versioned(version(name = "v1alpha1"))] +#[versioned( + version(name = "v1alpha1"), + crates( + kube_core = "kube::core", + k8s_openapi = "k8s_openapi", + schemars = "schemars", + ) +)] pub mod versioned { pub mod v1alpha1 { pub use v1alpha1_impl::ConnectionError; @@ -24,15 +31,10 @@ pub mod versioned { /// S3 connection definition as a resource. /// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3). - #[versioned(k8s( + #[versioned(crd( group = "s3.stackable.tech", kind = "S3Connection", plural = "s3connections", - crates( - kube_core = "kube::core", - k8s_openapi = "k8s_openapi", - schemars = "schemars", - ), namespaced ))] #[derive(CustomResource, Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] diff --git a/crates/stackable-versioned-macros/Cargo.toml b/crates/stackable-versioned-macros/Cargo.toml index fe6fe91a4..1712151a0 100644 --- a/crates/stackable-versioned-macros/Cargo.toml +++ b/crates/stackable-versioned-macros/Cargo.toml @@ -24,10 +24,6 @@ normal = ["k8s-openapi", "kube"] [lib] proc-macro = true -[features] -full = ["k8s"] -k8s = ["dep:kube", "dep:k8s-openapi"] - [dependencies] k8s-version = { path = "../k8s-version", features = ["darling"] } @@ -35,15 +31,15 @@ convert_case.workspace = true darling.workspace = true indoc.workspace = true itertools.workspace = true -k8s-openapi = { workspace = true, optional = true } -kube = { workspace = true, optional = true } +k8s-openapi.workspace = true +kube.workspace = true proc-macro2.workspace = true syn.workspace = true quote.workspace = true [dev-dependencies] # Only needed for doc tests / examples -stackable-versioned = { path = "../stackable-versioned", features = ["k8s"] } +stackable-versioned = { path = "../stackable-versioned" } insta.workspace = true prettyplease.workspace = true diff --git a/crates/stackable-versioned-macros/src/attrs/common.rs b/crates/stackable-versioned-macros/src/attrs/common.rs deleted file mode 100644 index 24ee40e87..000000000 --- a/crates/stackable-versioned-macros/src/attrs/common.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::ops::Deref; - -use darling::{ - Error, FromMeta, Result, - util::{Flag, Override as FlagOrOverride, SpannedValue}, -}; -use itertools::Itertools; -use k8s_version::Version; - -pub trait CommonOptions { - fn allow_unsorted(&self) -> Flag; -} - -#[derive(Debug, FromMeta)] -#[darling(and_then = CommonRootArguments::validate)] -pub struct CommonRootArguments -where - T: CommonOptions + Default, -{ - #[darling(default)] - pub options: T, - - #[darling(multiple, rename = "version")] - pub versions: SpannedValue>, -} - -impl CommonRootArguments -where - T: CommonOptions + Default, -{ - fn validate(mut self) -> Result { - let mut errors = Error::accumulator(); - - if self.versions.is_empty() { - errors.push( - Error::custom("at least one or more `version`s must be defined") - .with_span(&self.versions.span()), - ); - } - - let is_sorted = self.versions.iter().is_sorted_by_key(|v| v.name); - - // It needs to be sorted, even though the definition could be unsorted - // (if allow_unsorted is set). - self.versions.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); - - if !self.options.allow_unsorted().is_present() && !is_sorted { - let versions = self.versions.iter().map(|v| v.name).join(", "); - - errors.push(Error::custom(format!( - "versions must be defined in ascending order: {versions}", - ))); - } - - let duplicate_versions: Vec<_> = self - .versions - .iter() - .duplicates_by(|v| v.name) - .map(|v| v.name) - .collect(); - - if !duplicate_versions.is_empty() { - let versions = duplicate_versions.iter().join(", "); - - errors.push(Error::custom(format!( - "contains duplicate versions: {versions}", - ))); - } - - errors.finish_with(self) - } -} - -/// This struct contains supported version arguments. -/// -/// Supported arguments are: -/// -/// - `name` of the version, like `v1alpha1`. -/// - `deprecated` flag to mark that version as deprecated. -/// - `skip` option to skip generating various pieces of code. -/// - `doc` option to add version-specific documentation. -#[derive(Clone, Debug, FromMeta)] -pub struct VersionArguments { - pub deprecated: Option>, - pub skip: Option, - pub doc: Option, - pub name: Version, -} - -/// This struct contains supported common skip arguments. -/// -/// Supported arguments are: -/// -/// - `from` flag, which skips generating [`From`] implementations when provided. -#[derive(Clone, Debug, Default, FromMeta)] -pub struct SkipArguments { - /// Whether the [`From`] implementation generation should be skipped for all versions of this - /// container. - pub from: Flag, -} - -/// Wraps a value to indicate whether it is original or has been overridden. -#[derive(Clone, Debug)] -pub enum Override { - Default(T), - Explicit(T), -} - -impl FromMeta for Override -where - T: FromMeta, -{ - fn from_meta(item: &syn::Meta) -> Result { - FromMeta::from_meta(item).map(Override::Explicit) - } -} - -impl Deref for Override { - type Target = T; - - fn deref(&self) -> &Self::Target { - match &self { - Override::Default(inner) => inner, - Override::Explicit(inner) => inner, - } - } -} diff --git a/crates/stackable-versioned-macros/src/attrs/container.rs b/crates/stackable-versioned-macros/src/attrs/container.rs new file mode 100644 index 000000000..d75045ec6 --- /dev/null +++ b/crates/stackable-versioned-macros/src/attrs/container.rs @@ -0,0 +1,76 @@ +use darling::{Error, FromAttributes, FromMeta, Result, util::Flag}; +use syn::Path; + +#[derive(Debug, FromAttributes)] +#[darling(attributes(versioned), and_then = ContainerAttributes::validate)] +pub struct ContainerAttributes { + #[darling(rename = "crd")] + pub crd_arguments: Option, + + #[darling(default)] + pub skip: ContainerSkipArguments, +} + +impl ContainerAttributes { + fn validate(self) -> Result { + if self.crd_arguments.is_some() + && (self.skip.object_from.is_present() + || self.skip.merged_crd.is_present() + || self.skip.try_convert.is_present()) + { + return Err(Error::custom("spec sub structs can only use skip(from)")); + } + + Ok(self) + } +} + +#[derive(Debug, Default, FromMeta)] +pub struct ContainerSkipArguments { + pub from: Flag, + pub object_from: Flag, + pub merged_crd: Flag, + pub try_convert: Flag, +} + +/// This struct contains supported CRD arguments. +/// +/// The arguments are passed through to the `#[kube]` attribute. More details can be found in the +/// official docs: . +/// +/// Supported arguments are: +/// +/// - `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. +/// - `skip`: Controls skipping parts of the generation. +#[derive(Clone, Debug, FromMeta)] +pub struct StructCrdArguments { + pub group: String, + pub kind: Option, + pub singular: Option, + pub plural: Option, + pub namespaced: Flag, + // root + pub status: Option, + // derive + // schema + // scale + // printcolumn + #[darling(multiple, rename = "shortname")] + pub shortnames: Vec, + // category + // selectable + // doc + // annotation + // label +} diff --git a/crates/stackable-versioned-macros/src/attrs/container/k8s.rs b/crates/stackable-versioned-macros/src/attrs/container/k8s.rs deleted file mode 100644 index 88ca013d4..000000000 --- a/crates/stackable-versioned-macros/src/attrs/container/k8s.rs +++ /dev/null @@ -1,165 +0,0 @@ -use darling::{FromMeta, util::Flag}; -use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; -use syn::{Path, parse_quote}; - -use crate::attrs::common::Override; - -/// This struct contains supported Kubernetes arguments. -/// -/// The arguments are passed through to the `#[kube]` attribute. More details can be found in the -/// official docs: . -/// -/// Supported arguments are: -/// -/// - `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. -/// - `skip`: Controls skipping parts of the generation. -#[derive(Clone, Debug, FromMeta)] -pub struct KubernetesArguments { - pub group: String, - pub kind: Option, - pub singular: Option, - pub plural: Option, - pub namespaced: Flag, - // root - #[darling(default)] - pub crates: KubernetesCrateArguments, - pub status: Option, - // derive - // schema - // scale - // printcolumn - #[darling(multiple, rename = "shortname")] - pub shortnames: Vec, - // category - // selectable - // doc - // annotation - // label - #[darling(default)] - pub options: KubernetesConfigOptions, -} - -/// This struct contains crate overrides to be passed to `#[kube]`. -#[derive(Clone, Debug, FromMeta)] -pub struct KubernetesCrateArguments { - #[darling(default = default_kube_core)] - pub kube_core: Override, - - #[darling(default = default_kube_client)] - pub kube_client: Override, - - #[darling(default = default_k8s_openapi)] - pub k8s_openapi: Override, - - #[darling(default = default_schemars)] - pub schemars: Override, - - #[darling(default = default_serde)] - pub serde: Override, - - #[darling(default = default_serde_json)] - pub serde_json: Override, - - #[darling(default = default_versioned)] - pub versioned: Override, -} - -impl Default for KubernetesCrateArguments { - fn default() -> Self { - Self { - kube_core: default_kube_core(), - kube_client: default_kube_client(), - k8s_openapi: default_k8s_openapi(), - schemars: default_schemars(), - serde: default_serde(), - serde_json: default_serde_json(), - versioned: default_versioned(), - } - } -} - -fn default_kube_core() -> Override { - Override::Default(parse_quote! { ::kube::core }) -} - -fn default_kube_client() -> Override { - Override::Default(parse_quote! { ::kube::client }) -} - -fn default_k8s_openapi() -> Override { - Override::Default(parse_quote! { ::k8s_openapi }) -} - -fn default_schemars() -> Override { - Override::Default(parse_quote! { ::schemars }) -} - -fn default_serde() -> Override { - Override::Default(parse_quote! { ::serde }) -} - -fn default_serde_json() -> Override { - Override::Default(parse_quote! { ::serde_json }) -} - -fn default_versioned() -> Override { - Override::Default(parse_quote! { ::stackable_versioned }) -} - -impl ToTokens for KubernetesCrateArguments { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let mut crate_overrides = TokenStream::new(); - - let KubernetesCrateArguments { - kube_client: _, - k8s_openapi, - serde_json, - kube_core, - schemars, - serde, - .. - } = self; - - if let Override::Explicit(k8s_openapi) = k8s_openapi { - crate_overrides.extend(quote! { k8s_openapi = #k8s_openapi, }); - } - - if let Override::Explicit(serde_json) = serde_json { - crate_overrides.extend(quote! { serde_json = #serde_json, }); - } - - if let Override::Explicit(kube_core) = kube_core { - crate_overrides.extend(quote! { kube_core = #kube_core, }); - } - - if let Override::Explicit(schemars) = schemars { - crate_overrides.extend(quote! { schemars = #schemars, }); - } - - if let Override::Explicit(serde) = serde { - crate_overrides.extend(quote! { serde = #serde, }); - } - - if !crate_overrides.is_empty() { - tokens.extend(quote! { , crates(#crate_overrides) }); - } - } -} - -#[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/attrs/container/mod.rs b/crates/stackable-versioned-macros/src/attrs/container/mod.rs deleted file mode 100644 index f43a06866..000000000 --- a/crates/stackable-versioned-macros/src/attrs/container/mod.rs +++ /dev/null @@ -1,72 +0,0 @@ -use darling::{Error, FromAttributes, FromMeta, Result, util::Flag}; - -use crate::attrs::{ - common::{CommonOptions, CommonRootArguments, SkipArguments}, - container::k8s::KubernetesArguments, -}; - -pub mod k8s; - -#[derive(Debug, FromMeta)] -#[darling(and_then = StandaloneContainerAttributes::validate)] -pub struct StandaloneContainerAttributes { - #[darling(rename = "k8s")] - pub kubernetes_arguments: Option, - - #[darling(flatten)] - pub common: CommonRootArguments, -} - -impl StandaloneContainerAttributes { - fn validate(self) -> Result { - if self.kubernetes_arguments.is_some() && cfg!(not(feature = "k8s")) { - return Err(Error::custom( - "the `#[versioned(k8s())]` attribute can only be used when the `k8s` feature is enabled", - )); - } - - Ok(self) - } -} - -#[derive(Debug, FromMeta, Default)] -pub struct StandaloneContainerOptions { - pub allow_unsorted: Flag, - pub skip: Option, -} - -impl CommonOptions for StandaloneContainerOptions { - fn allow_unsorted(&self) -> Flag { - self.allow_unsorted - } -} - -#[derive(Debug, FromAttributes)] -#[darling( - attributes(versioned), - and_then = NestedContainerAttributes::validate -)] -pub struct NestedContainerAttributes { - #[darling(rename = "k8s")] - pub kubernetes_arguments: Option, - - #[darling(default)] - pub options: NestedContainerOptionArguments, -} - -impl NestedContainerAttributes { - fn validate(self) -> Result { - if self.kubernetes_arguments.is_some() && cfg!(not(feature = "k8s")) { - return Err(Error::custom( - "the `#[versioned(k8s())]` attribute can only be used when the `k8s` feature is enabled", - )); - } - - Ok(self) - } -} - -#[derive(Debug, Default, FromMeta)] -pub struct NestedContainerOptionArguments { - pub skip: Option, -} diff --git a/crates/stackable-versioned-macros/src/attrs/mod.rs b/crates/stackable-versioned-macros/src/attrs/mod.rs index 4442ca4f6..1a484e0b7 100644 --- a/crates/stackable-versioned-macros/src/attrs/mod.rs +++ b/crates/stackable-versioned-macros/src/attrs/mod.rs @@ -1,4 +1,3 @@ -pub mod common; pub mod container; pub mod item; pub mod module; diff --git a/crates/stackable-versioned-macros/src/attrs/module.rs b/crates/stackable-versioned-macros/src/attrs/module.rs index addc1a93c..71d61d34b 100644 --- a/crates/stackable-versioned-macros/src/attrs/module.rs +++ b/crates/stackable-versioned-macros/src/attrs/module.rs @@ -1,22 +1,264 @@ -use darling::{FromMeta, util::Flag}; +use std::ops::Deref; -use crate::attrs::common::{CommonOptions, CommonRootArguments, SkipArguments}; +use darling::{ + Error, FromMeta, Result, + util::{Flag, Override as FlagOrOverride, SpannedValue}, +}; +use itertools::Itertools as _; +use k8s_version::Version; +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; +use syn::{Path, parse_quote}; #[derive(Debug, FromMeta)] +#[darling(and_then = ModuleAttributes::validate)] pub struct ModuleAttributes { - #[darling(flatten)] - pub common: CommonRootArguments, + #[darling(multiple, rename = "version")] + pub versions: SpannedValue>, + + #[darling(default)] + pub crates: CrateArguments, + + #[darling(default)] + pub options: ModuleOptions, + + #[darling(default)] + pub skip: ModuleSkipArguments, +} + +impl ModuleAttributes { + fn validate(mut self) -> Result { + let mut errors = Error::accumulator(); + + if self.versions.is_empty() { + errors.push( + Error::custom("at least one or more `version`s must be defined") + .with_span(&self.versions.span()), + ); + } + + let is_sorted = self.versions.iter().is_sorted_by_key(|v| v.name); + + // It needs to be sorted, even though the definition could be unsorted + // (if allow_unsorted is set). + self.versions.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); + + if !self.options.common.allow_unsorted.is_present() && !is_sorted { + let versions = self.versions.iter().map(|v| v.name).join(", "); + + errors.push(Error::custom(format!( + "versions must be defined in ascending order: {versions}", + ))); + } + + let duplicate_versions: Vec<_> = self + .versions + .iter() + .duplicates_by(|v| v.name) + .map(|v| v.name) + .collect(); + + if !duplicate_versions.is_empty() { + let versions = duplicate_versions.iter().join(", "); + + errors.push(Error::custom(format!( + "contains duplicate versions: {versions}", + ))); + } + + errors.finish_with(self) + } } -#[derive(Debug, FromMeta, Default)] +#[derive(Debug, Default, FromMeta)] pub struct ModuleOptions { + #[darling(flatten)] + pub common: ModuleCommonOptions, + + #[darling(default, rename = "k8s")] + pub kubernetes: KubernetesConfigOptions, +} + +#[derive(Debug, Default, FromMeta)] +pub struct ModuleCommonOptions { pub allow_unsorted: Flag, - pub skip: Option, pub preserve_module: Flag, } -impl CommonOptions for ModuleOptions { - fn allow_unsorted(&self) -> Flag { - self.allow_unsorted +#[derive(Debug, Default, FromMeta)] +pub struct ModuleSkipArguments { + pub from: Flag, + pub object_from: Flag, + pub merged_crd: Flag, + pub try_convert: Flag, +} + +/// This struct contains crate overrides to be passed to `#[kube]`. +#[derive(Clone, Debug, FromMeta)] +pub struct CrateArguments { + #[darling(default = default_kube_core)] + pub kube_core: Override, + + #[darling(default = default_kube_client)] + pub kube_client: Override, + + #[darling(default = default_k8s_openapi)] + pub k8s_openapi: Override, + + #[darling(default = default_schemars)] + pub schemars: Override, + + #[darling(default = default_serde)] + pub serde: Override, + + #[darling(default = default_serde_json)] + pub serde_json: Override, + + #[darling(default = default_serde_yaml)] + pub serde_yaml: Override, + + #[darling(default = default_versioned)] + pub versioned: Override, +} + +impl Default for CrateArguments { + fn default() -> Self { + Self { + kube_core: default_kube_core(), + kube_client: default_kube_client(), + k8s_openapi: default_k8s_openapi(), + schemars: default_schemars(), + serde: default_serde(), + serde_json: default_serde_json(), + serde_yaml: default_serde_yaml(), + versioned: default_versioned(), + } + } +} + +fn default_kube_core() -> Override { + Override::Default(parse_quote! { ::kube::core }) +} + +fn default_kube_client() -> Override { + Override::Default(parse_quote! { ::kube::client }) +} + +fn default_k8s_openapi() -> Override { + Override::Default(parse_quote! { ::k8s_openapi }) +} + +fn default_schemars() -> Override { + Override::Default(parse_quote! { ::schemars }) +} + +fn default_serde() -> Override { + Override::Default(parse_quote! { ::serde }) +} + +fn default_serde_json() -> Override { + Override::Default(parse_quote! { ::serde_json }) +} + +fn default_serde_yaml() -> Override { + Override::Default(parse_quote! { ::serde_yaml }) +} + +fn default_versioned() -> Override { + Override::Default(parse_quote! { ::stackable_versioned }) +} + +impl ToTokens for CrateArguments { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let mut crate_overrides = TokenStream::new(); + + let CrateArguments { + kube_client: _, + k8s_openapi, + serde_json, + kube_core, + schemars, + serde, + .. + } = self; + + if let Override::Explicit(k8s_openapi) = k8s_openapi { + crate_overrides.extend(quote! { k8s_openapi = #k8s_openapi, }); + } + + if let Override::Explicit(serde_json) = serde_json { + crate_overrides.extend(quote! { serde_json = #serde_json, }); + } + + if let Override::Explicit(kube_core) = kube_core { + crate_overrides.extend(quote! { kube_core = #kube_core, }); + } + + if let Override::Explicit(schemars) = schemars { + crate_overrides.extend(quote! { schemars = #schemars, }); + } + + if let Override::Explicit(serde) = serde { + crate_overrides.extend(quote! { serde = #serde, }); + } + + if !crate_overrides.is_empty() { + tokens.extend(quote! { , crates(#crate_overrides) }); + } + } +} + +#[derive(Clone, Default, Debug, FromMeta)] +pub struct KubernetesConfigOptions { + pub experimental_conversion_tracking: Flag, + pub enable_tracing: Flag, +} + +/// This struct contains supported version arguments. +/// +/// Supported arguments are: +/// +/// - `name` of the version, like `v1alpha1`. +/// - `deprecated` flag to mark that version as deprecated. +/// - `skip` option to skip generating various pieces of code. +/// - `doc` option to add version-specific documentation. +#[derive(Clone, Debug, FromMeta)] +pub struct VersionArguments { + pub deprecated: Option>, + pub skip: Option, + pub doc: Option, + pub name: Version, +} + +#[derive(Clone, Debug, FromMeta)] +pub struct VersionSkipArguments { + pub from: Flag, + pub object_from: Flag, +} + +/// Wraps a value to indicate whether it is original or has been overridden. +#[derive(Clone, Debug)] +pub enum Override { + Default(T), + Explicit(T), +} + +impl FromMeta for Override +where + T: FromMeta, +{ + fn from_meta(item: &syn::Meta) -> Result { + FromMeta::from_meta(item).map(Override::Explicit) + } +} + +impl Deref for Override { + type Target = T; + + fn deref(&self) -> &Self::Target { + match &self { + Override::Default(inner) => inner, + Override::Explicit(inner) => inner, + } } } diff --git a/crates/stackable-versioned-macros/src/codegen/container/enum.rs b/crates/stackable-versioned-macros/src/codegen/container/enum.rs index 278145435..45c17c3a8 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/enum.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/enum.rs @@ -6,55 +6,21 @@ use quote::quote; use syn::{Generics, ItemEnum}; use crate::{ - attrs::container::NestedContainerAttributes, + attrs::container::ContainerAttributes, codegen::{ - ItemStatus, StandaloneContainerAttributes, VersionDefinition, + ItemStatus, VersionDefinition, changes::Neighbors, - container::{CommonContainerData, Container, ContainerIdents, ContainerOptions}, + container::{ + CommonContainerData, Container, ContainerIdents, ContainerOptions, ContainerTokens, + Direction, ExtendContainerTokens, ModuleGenerationContext, VersionContext, + }, item::VersionedVariant, }, }; impl Container { - pub fn new_standalone_enum( - item_enum: ItemEnum, - attributes: StandaloneContainerAttributes, - versions: &[VersionDefinition], - ) -> Result { - let mut versioned_variants = Vec::new(); - for variant in item_enum.variants { - let mut versioned_variant = VersionedVariant::new(variant, versions)?; - versioned_variant.insert_container_versions(versions); - versioned_variants.push(versioned_variant); - } - - let options = ContainerOptions { - kubernetes_arguments: None, - skip_from: attributes - .common - .options - .skip - .is_some_and(|s| s.from.is_present()), - }; - - let idents = ContainerIdents::from(item_enum.ident, None); - - let common = CommonContainerData { - original_attributes: item_enum.attrs, - options, - idents, - }; - - Ok(Self::Enum(Enum { - generics: item_enum.generics, - variants: versioned_variants, - common, - })) - } - - // TODO (@Techassi): See what can be unified into a single 'new' function - pub fn new_enum_nested(item_enum: ItemEnum, versions: &[VersionDefinition]) -> Result { - let attributes = NestedContainerAttributes::from_attributes(&item_enum.attrs)?; + pub fn new_enum(item_enum: ItemEnum, versions: &[VersionDefinition]) -> Result { + let attributes = ContainerAttributes::from_attributes(&item_enum.attrs)?; let mut versioned_variants = Vec::new(); for variant in item_enum.variants { @@ -64,11 +30,13 @@ impl Container { } let options = ContainerOptions { - kubernetes_arguments: None, - skip_from: attributes.options.skip.is_some_and(|s| s.from.is_present()), + skip_from: attributes.skip.from.is_present(), + skip_object_from: attributes.skip.object_from.is_present(), + skip_merged_crd: attributes.skip.merged_crd.is_present(), + skip_try_convert: attributes.skip.try_convert.is_present(), }; - let idents = ContainerIdents::from(item_enum.ident, None); + let idents = ContainerIdents::from(item_enum.ident); let common = CommonContainerData { original_attributes: item_enum.attrs, @@ -99,18 +67,43 @@ pub struct Enum { // Common token generation impl Enum { + pub fn generate_tokens<'a>( + &'a self, + versions: &'a [VersionDefinition], + gen_ctx: ModuleGenerationContext<'a>, + ) -> ContainerTokens<'a> { + let mut versions = versions.iter().peekable(); + let mut container_tokens = ContainerTokens::default(); + + while let Some(version) = versions.next() { + let next_version = versions.peek().copied(); + let ver_ctx = VersionContext::new(version, next_version); + + let enum_definition = self.generate_definition(ver_ctx); + let upgrade_from = self.generate_from_impl(Direction::Upgrade, ver_ctx, gen_ctx); + let downgrade_from = self.generate_from_impl(Direction::Downgrade, ver_ctx, gen_ctx); + + container_tokens + .extend_inner(&version.inner, enum_definition) + .extend_between(&version.inner, upgrade_from) + .extend_between(&version.inner, downgrade_from); + } + + container_tokens + } + /// Generates code for the enum definition. - pub fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { + pub fn generate_definition(&self, ver_ctx: VersionContext<'_>) -> TokenStream { let where_clause = self.generics.where_clause.as_ref(); let type_generics = &self.generics; let original_attributes = &self.common.original_attributes; let ident = &self.common.idents.original; - let version_docs = &version.docs; + let version_docs = &ver_ctx.version.docs; let mut variants = TokenStream::new(); for variant in &self.variants { - variants.extend(variant.generate_for_container(version)); + variants.extend(variant.generate_for_container(ver_ctx.version)); } quote! { @@ -122,130 +115,82 @@ impl Enum { } } - /// Generates code for the `From for NextVersion` implementation. - pub fn generate_upgrade_from_impl( - &self, - version: &VersionDefinition, - next_version: Option<&VersionDefinition>, - add_attributes: bool, - ) -> Option { - if version.skip_from || self.common.options.skip_from { - return None; - } - - match next_version { - Some(next_version) => { - // TODO (@Techassi): Support generic types which have been removed in newer versions, - // but need to exist for older versions How do we represent that? Because the - // defined struct always represents the latest version. I guess we could generally - // 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 for_module_ident = &next_version.idents.module; - let from_module_ident = &version.idents.module; - - let variants: TokenStream = self - .variants - .iter() - .filter_map(|v| { - v.generate_for_upgrade_from_impl(version, next_version, enum_ident) - }) - .collect(); - - // Include allow(deprecated) only when this or the next version is - // deprecated. Also include it, when a variant in this or the next - // version is deprecated. - let allow_attribute = (version.deprecated.is_some() - || next_version.deprecated.is_some() - || self.is_any_variant_deprecated(version) - || self.is_any_variant_deprecated(next_version)) - .then_some(quote! { #[allow(deprecated)] }); - - // Only add the #[automatically_derived] attribute only if this impl is used - // outside of a module (in standalone mode). - let automatically_derived = add_attributes - .not() - .then(|| quote! {#[automatically_derived]}); - - Some(quote! { - #automatically_derived - #allow_attribute - impl #impl_generics ::std::convert::From<#from_module_ident::#enum_ident #type_generics> for #for_module_ident::#enum_ident #type_generics - #where_clause - { - fn from(#from_enum_ident: #from_module_ident::#enum_ident #type_generics) -> Self { - match #from_enum_ident { - #variants - } - } - } - }) - } - None => None, - } - } - - pub fn generate_downgrade_from_impl( + // TODO (@Techassi): Add doc comments + pub fn generate_from_impl( &self, - version: &VersionDefinition, - next_version: Option<&VersionDefinition>, - add_attributes: bool, + direction: Direction, + ver_ctx: VersionContext<'_>, + gen_ctx: ModuleGenerationContext<'_>, ) -> Option { - if version.skip_from || self.common.options.skip_from { + if ver_ctx.version.skip_from || self.common.options.skip_from { return None; } - 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_module_ident = &next_version.idents.module; - let for_module_ident = &version.idents.module; - - let variants: TokenStream = self - .variants + let version = ver_ctx.version; + + ver_ctx.next_version.map(|next_version| { + // TODO (@Techassi): Support generic types which have been removed in newer versions, + // but need to exist for older versions How do we represent that? Because the + // defined struct always represents the latest version. I guess we could generally + // 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; + + // Include allow(deprecated) only when this or the next version is + // deprecated. Also include it, when a variant in this or the next + // version is deprecated. + let allow_attribute = (version.deprecated.is_some() + || next_version.deprecated.is_some() + || self.is_any_variant_deprecated(version) + || self.is_any_variant_deprecated(next_version)) + .then_some(quote! { #[allow(deprecated)] }); + + // Only add the #[automatically_derived] attribute only if this impl is used + // outside of a module (in standalone mode). + let automatically_derived = gen_ctx.add_attributes + .not() + .then(|| quote! {#[automatically_derived]}); + + let variants = |direction: Direction| -> TokenStream { + self.variants .iter() .filter_map(|v| { - v.generate_for_downgrade_from_impl(version, next_version, enum_ident) + v.generate_for_from_impl(direction, version, next_version, enum_ident) }) - .collect(); - - // Include allow(deprecated) only when this or the next version is - // deprecated. Also include it, when a variant in this or the next - // version is deprecated. - let allow_attribute = (version.deprecated.is_some() - || next_version.deprecated.is_some() - || self.is_any_variant_deprecated(version) - || self.is_any_variant_deprecated(next_version)) - .then_some(quote! { #[allow(deprecated)] }); - - // Only add the #[automatically_derived] attribute only if this impl is used - // outside of a module (in standalone mode). - let automatically_derived = add_attributes - .not() - .then(|| quote! {#[automatically_derived]}); - - Some(quote! { - #automatically_derived - #allow_attribute - impl #impl_generics ::std::convert::From<#from_module_ident::#enum_ident #type_generics> for #for_module_ident::#enum_ident #type_generics - #where_clause - { - fn from(#from_enum_ident: #from_module_ident::#enum_ident #type_generics) -> Self { - match #from_enum_ident { - #variants - } + .collect() + }; + + let (variants, for_module_ident, from_module_ident) = match direction { + Direction::Upgrade => { + let for_module_ident = &next_version.idents.module; + let from_module_ident = &version.idents.module; + + (variants(Direction::Upgrade), for_module_ident, from_module_ident) + }, + Direction::Downgrade => { + let for_module_ident = &version.idents.module; + let from_module_ident = &next_version.idents.module; + + (variants(Direction::Downgrade), for_module_ident, from_module_ident) + }, + }; + + quote! { + #automatically_derived + #allow_attribute + impl #impl_generics ::std::convert::From<#from_module_ident::#enum_ident #type_generics> for #for_module_ident::#enum_ident #type_generics + #where_clause + { + fn from(#from_enum_ident: #from_module_ident::#enum_ident #type_generics) -> Self { + match #from_enum_ident { + #variants } } - }) + } } - None => None, - } + }) } /// Returns whether any variant is deprecated in the provided `version`. diff --git a/crates/stackable-versioned-macros/src/codegen/container/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/mod.rs index ed72fbe46..2b4a13a56 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/mod.rs @@ -1,12 +1,18 @@ -use darling::{Result, util::IdentString}; -use proc_macro2::{Span, TokenStream}; -use quote::{format_ident, quote}; -use syn::{Attribute, Ident, ItemEnum, ItemStruct, Visibility}; +use std::collections::HashMap; + +use darling::util::IdentString; +use k8s_version::Version; +use proc_macro2::{Span, TokenStream, TokenTree}; +use quote::format_ident; +use syn::{Attribute, Ident, Visibility}; use crate::{ - attrs::container::{StandaloneContainerAttributes, k8s::KubernetesArguments}, + attrs::{ + container::StructCrdArguments, + module::{CrateArguments, KubernetesConfigOptions, ModuleSkipArguments}, + }, codegen::{ - KubernetesTokens, VersionDefinition, + VersionDefinition, container::{r#enum::Enum, r#struct::Struct}, }, utils::ContainerIdentExt, @@ -37,73 +43,117 @@ pub enum Container { Enum(Enum), } -impl Container { - /// Generates the container definition for the specified `version`. - pub fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { - match self { - Container::Struct(s) => s.generate_definition(version), - Container::Enum(e) => e.generate_definition(version), - } +#[derive(Debug, Default)] +pub struct ContainerTokens<'a> { + pub versioned: HashMap<&'a Version, VersionedContainerTokens>, + pub outer: TokenStream, +} + +#[derive(Debug, Default)] +/// A collection of generated tokens for a container per version. +pub struct VersionedContainerTokens { + /// The inner tokens are placed inside the version module. These tokens mostly only include the + /// container definition with attributes, doc comments, etc. + pub inner: TokenStream, + + /// These tokens are placed between version modules. These could technically be grouped together + /// with the outer tokens, but it makes sense to keep them separate to achieve a more structured + /// code generation. These tokens mostly only include `From` impls to convert between two versions + pub between: TokenStream, +} + +pub trait ExtendContainerTokens<'a, T> { + fn extend_inner>( + &mut self, + version: &'a Version, + streams: I, + ) -> &mut Self; + fn extend_between>( + &mut self, + version: &'a Version, + streams: I, + ) -> &mut Self; + fn extend_outer>(&mut self, streams: I) -> &mut Self; +} + +impl<'a> ExtendContainerTokens<'a, TokenStream> for ContainerTokens<'a> { + fn extend_inner>( + &mut self, + version: &'a Version, + streams: I, + ) -> &mut Self { + self.versioned + .entry(version) + .or_default() + .inner + .extend(streams); + self } - /// Generates the `From for NextVersion` implementation for the container. - pub fn generate_upgrade_from_impl( - &self, - version: &VersionDefinition, - next_version: Option<&VersionDefinition>, - add_attributes: bool, - ) -> Option { - match self { - Container::Struct(s) => { - s.generate_upgrade_from_impl(version, next_version, add_attributes) - } - Container::Enum(e) => { - e.generate_upgrade_from_impl(version, next_version, add_attributes) - } - } + fn extend_between>( + &mut self, + version: &'a Version, + streams: I, + ) -> &mut Self { + self.versioned + .entry(version) + .or_default() + .between + .extend(streams); + self } - /// Generates the `From for Version` implementation for the container. - pub fn generate_downgrade_from_impl( - &self, - version: &VersionDefinition, - next_version: Option<&VersionDefinition>, - add_attributes: bool, - ) -> Option { - match self { - Container::Struct(s) => { - s.generate_downgrade_from_impl(version, next_version, add_attributes) - } - Container::Enum(e) => { - e.generate_downgrade_from_impl(version, next_version, add_attributes) - } - } + fn extend_outer>(&mut self, streams: I) -> &mut Self { + self.outer.extend(streams); + self } +} - /// Generates Kubernetes specific code for the container. - /// - /// This includes CRD merging, CRD conversion, and the conversion tracking status struct. - pub fn generate_kubernetes_code( - &self, - versions: &[VersionDefinition], - tokens: &KubernetesTokens, - vis: &Visibility, - is_nested: bool, - ) -> Option { - match self { - Container::Struct(s) => s.generate_kubernetes_code(versions, tokens, vis, is_nested), - Container::Enum(_) => None, - } +impl<'a> ExtendContainerTokens<'a, TokenTree> for ContainerTokens<'a> { + fn extend_inner>( + &mut self, + version: &'a Version, + streams: I, + ) -> &mut Self { + self.versioned + .entry(version) + .or_default() + .inner + .extend(streams); + self } - /// Generates KUbernetes specific code for individual versions. - pub fn generate_kubernetes_version_items( - &self, - version: &VersionDefinition, - ) -> Option<(TokenStream, IdentString, TokenStream, String)> { + fn extend_between>( + &mut self, + version: &'a Version, + streams: I, + ) -> &mut Self { + self.versioned + .entry(version) + .or_default() + .between + .extend(streams); + self + } + + fn extend_outer>(&mut self, streams: I) -> &mut Self { + self.outer.extend(streams); + self + } +} + +impl Container { + // TODO (@Techassi): Only have a single function here. It should return and store all generated + // tokens. It should also have access to a single GenerationContext, which provides all external + // parameters which influence code generation. + pub fn generate_tokens<'a>( + &'a self, + versions: &'a [VersionDefinition], + ctx: ModuleGenerationContext<'a>, + ) -> ContainerTokens<'a> { match self { - Container::Struct(s) => s.generate_kubernetes_version_items(version), - Container::Enum(_) => None, + Container::Struct(s) => s.generate_tokens(versions, ctx), + Container::Enum(e) => e.generate_tokens(versions, ctx), } } @@ -116,174 +166,105 @@ impl Container { } } -/// A versioned standalone container. -/// -/// A standalone container is a container defined outside of a versioned module. See [`Module`][1] -/// for more information about versioned modules. -/// -/// [1]: crate::codegen::module::Module -pub struct StandaloneContainer { - versions: Vec, - container: Container, - vis: Visibility, -} - -impl StandaloneContainer { - /// Creates a new versioned standalone struct. - pub fn new_struct( - item_struct: ItemStruct, - attributes: StandaloneContainerAttributes, - ) -> Result { - let versions: Vec<_> = (&attributes).into(); - let vis = item_struct.vis.clone(); - - let container = Container::new_standalone_struct(item_struct, attributes, &versions)?; - - Ok(Self { - container, - versions, - vis, - }) - } - - /// Creates a new versioned standalone enum. - pub fn new_enum( - item_enum: ItemEnum, - attributes: StandaloneContainerAttributes, - ) -> Result { - let versions: Vec<_> = (&attributes).into(); - let vis = item_enum.vis.clone(); - - let container = Container::new_standalone_enum(item_enum, attributes, &versions)?; - - Ok(Self { - container, - versions, - vis, - }) - } - - /// Generate tokens containing every piece of code required for a standalone container. - pub fn generate_tokens(&self) -> TokenStream { - let vis = &self.vis; - - let mut kubernetes_tokens = KubernetesTokens::default(); - let mut tokens = TokenStream::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 '&&'. - let next_version = versions.peek().copied(); - - // Generate the From impl for upgrading the CRD. - let upgrade_from_impl = - self.container - .generate_upgrade_from_impl(version, next_version, false); - - // Generate the From impl for downgrading the CRD. - let downgrade_from_impl = - self.container - .generate_downgrade_from_impl(version, next_version, false); - - // Add the #[deprecated] attribute when the version is marked as deprecated. - let deprecated_attribute = version - .deprecated - .as_ref() - .map(|note| quote! { #[deprecated = #note] }); - - // 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); - } - - tokens.extend(quote! { - #[automatically_derived] - #deprecated_attribute - #vis mod #module_ident { - use super::*; - #container_definition - } - - #upgrade_from_impl - #downgrade_from_impl - }); - } - - // Finally add tokens outside of the container definitions - tokens.extend(self.container.generate_kubernetes_code( - &self.versions, - &kubernetes_tokens, - vis, - false, - )); +/// A collection of container idents used for different purposes. +#[derive(Debug)] +pub struct ContainerIdents { + /// The original ident, or name, of the versioned container. + pub original: IdentString, - tokens - } + /// The ident used as a parameter. + pub parameter: IdentString, } -/// A collection of container idents used for different purposes. #[derive(Debug)] -pub struct ContainerIdents { +pub struct KubernetesIdents { /// 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, + pub kind: 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, + pub 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, + pub 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 as a parameter. pub parameter: IdentString, } -impl ContainerIdents { - pub fn from(ident: Ident, kubernetes_arguments: Option<&KubernetesArguments>) -> Self { - 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(), - }, +impl From for ContainerIdents { + fn from(ident: Ident) -> Self { + Self { + parameter: ident.as_parameter_ident(), + original: ident.into(), + } + } +} + +impl KubernetesIdents { + pub fn from(ident: &IdentString, arguments: &StructCrdArguments) -> Self { + let kind = match &arguments.kind { + Some(kind) => IdentString::from(Ident::new(kind, Span::call_site())), 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(); + let status = IdentString::from(format_ident!("{kind}StatusWithChangedValues")); + let version = IdentString::from(format_ident!("{kind}Version")); + let parameter = kind.as_parameter_ident(); Self { - parameter: ident.as_parameter_ident(), - original: ident.into(), - kubernetes_parameter, - kubernetes_version, - kubernetes_status, - kubernetes, + parameter, + version, + status, + kind, } } } #[derive(Debug)] pub struct ContainerOptions { - pub kubernetes_arguments: Option, pub skip_from: bool, + pub skip_object_from: bool, + pub skip_merged_crd: bool, + pub skip_try_convert: bool, +} + +/// Describes the direction of [`From`] implementations. +#[derive(Copy, Clone, Debug)] +pub enum Direction { + Upgrade, + Downgrade, +} + +#[derive(Clone, Copy, Debug)] +pub struct ModuleGenerationContext<'a> { + pub kubernetes_options: &'a KubernetesConfigOptions, + pub skip: &'a ModuleSkipArguments, + pub crates: &'a CrateArguments, + pub vis: &'a Visibility, + + pub add_attributes: bool, +} + +#[derive(Clone, Copy, Debug)] +pub struct VersionContext<'a> { + pub version: &'a VersionDefinition, + pub next_version: Option<&'a VersionDefinition>, +} + +impl<'a> VersionContext<'a> { + pub fn new( + version: &'a VersionDefinition, + next_version: Option<&'a VersionDefinition>, + ) -> Self { + Self { + version, + next_version, + } + } } diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs new file mode 100644 index 000000000..d2e09f120 --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/conversion.rs @@ -0,0 +1,547 @@ +use std::{borrow::Cow, cmp::Ordering}; + +use indoc::formatdoc; +use itertools::Itertools as _; +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse_quote; + +use crate::{ + codegen::{ + VersionDefinition, + container::{ + ModuleGenerationContext, + r#struct::{SpecGenerationContext, Struct}, + }, + }, + utils::{doc_comments::DocComments as _, 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"; + +#[derive(Debug, Default)] +pub struct TracingTokens { + pub successful_conversion_response_event: Option, + pub convert_objects_instrumentation: Option, + pub invalid_conversion_review_event: Option, + pub try_convert_instrumentation: Option, +} + +impl Struct { + pub(super) fn generate_try_convert_fn( + &self, + versions: &[VersionDefinition], + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> Option { + if mod_gen_ctx.skip.try_convert.is_present() || self.common.options.skip_try_convert { + return None; + } + + let version_enum_ident = &spec_gen_ctx.kubernetes_idents.version; + let struct_ident = &spec_gen_ctx.kubernetes_idents.kind; + + let kube_client_path = &*mod_gen_ctx.crates.kube_client; + let serde_json_path = &*mod_gen_ctx.crates.serde_json; + let kube_core_path = &*mod_gen_ctx.crates.kube_core; + let versioned_path = &*mod_gen_ctx.crates.versioned; + + let convert_object_error = quote! { #versioned_path::ConvertObjectError }; + + // Generate conversion paths and the match arms for these paths + let conversion_match_arms = + self.generate_conversion_match_arms(versions, mod_gen_ctx, spec_gen_ctx); + + // 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_conversion_tracing(mod_gen_ctx, spec_gen_ctx); + + // 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() + } + }; + + // Convert all objects into the desired version + let response = match Self::convert_objects(request.objects, &request.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 desired_api_version = #version_enum_ident::from_api_version(desired_api_version) + .map_err(|source| #convert_object_error::ParseDesiredApiVersion { source })?; + + 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_object(object.clone()) + .map_err(|source| #convert_object_error::Parse { source })?; + + match (current_object, desired_api_version) { + #(#conversion_match_arms,)* + // In case the desired version matches the current object api version, we + // don't need to do anything. + // + // NOTE (@Techassi): I'm curious if this will ever happen? In theory the K8s + // apiserver should never send such a conversion review. + // + // Note(@sbernauer): I would prefer to explicitly list the remaining no-op + // cases, so the compiler ensures we did not miss a conversion + // // let version_idents = versions.iter().map(|v| &v.idents.variant); + // #( + // (Self::#version_idents(_), #version_enum_ident::#version_idents) + // )|* => converted_objects.push(object) + _ => converted_objects.push(object), + } + } + + ::std::result::Result::Ok(converted_objects) + } + }) + } + + pub(super) fn generate_status_struct( + &self, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> Option { + if mod_gen_ctx.skip.try_convert.is_present() || self.common.options.skip_try_convert { + return None; + } + + let status_ident = &spec_gen_ctx.kubernetes_idents.status; + + let versioned_path = &*mod_gen_ctx.crates.versioned; + let schemars_path = &*mod_gen_ctx.crates.schemars; + let serde_path = &*mod_gen_ctx.crates.serde; + + // TODO (@Techassi): Validate that users don't specify the status we generate + let status = spec_gen_ctx + .kubernetes_arguments + .status + .as_ref() + .map(|status| { + quote! { + #[serde(flatten)] + pub status: #status, + } + }); + + Some(quote! { + #[derive( + ::core::clone::Clone, + ::core::default::Default, + ::core::fmt::Debug, + #serde_path::Deserialize, + #serde_path::Serialize, + #schemars_path::JsonSchema + )] + #[serde(rename_all = "camelCase")] + pub struct #status_ident { + pub changed_values: #versioned_path::ChangedValues, + + #status + } + + impl #versioned_path::TrackingStatus for #status_ident { + fn changes(&mut self) -> &mut #versioned_path::ChangedValues { + &mut self.changed_values + } + } + }) + } + + pub(super) fn generate_from_json_object_fn( + &self, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> Option { + if mod_gen_ctx.skip.try_convert.is_present() || self.common.options.skip_try_convert { + return None; + } + + let serde_json_path = &*mod_gen_ctx.crates.serde_json; + let versioned_path = &*mod_gen_ctx.crates.versioned; + + let parse_object_error = quote! { #versioned_path::ParseObjectError }; + let enum_ident_string = spec_gen_ctx.kubernetes_idents.kind.to_string(); + + let version_strings = &spec_gen_ctx.version_strings; + let variant_idents = &spec_gen_ctx.variant_idents; + + let api_versions = version_strings.iter().map(|version| { + format!( + "{group}/{version}", + group = &spec_gen_ctx.kubernetes_arguments.group + ) + }); + + Some(quote! { + fn from_json_object(object_value: #serde_json_path::Value) -> ::std::result::Result { + let object_kind = object_value + .get("kind") + .ok_or_else(|| #parse_object_error::FieldMissing{ field: "kind".to_owned() })? + .as_str() + .ok_or_else(|| #parse_object_error::FieldNotStr{ field: "kind".to_owned() })?; + + // Note(@sbernauer): The kind must be checked here, because it is possible for the + // wrong object to be deserialized. Checking here stops us assuming the kind is + // correct and accidentally updating upgrade/downgrade information in the status in + // a later step. + if object_kind != #enum_ident_string { + return Err(#parse_object_error::UnexpectedKind{ + kind: object_kind.to_owned(), + expected: #enum_ident_string.to_owned(), + }); + } + + let api_version = object_value + .get("apiVersion") + .ok_or_else(|| #parse_object_error::FieldMissing{ field: "apiVersion".to_owned() })? + .as_str() + .ok_or_else(|| #parse_object_error::FieldNotStr{ field: "apiVersion".to_owned() })?; + + let object = match api_version { + #(#api_versions => { + let object = #serde_json_path::from_value(object_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) + } + }) + } + + pub(super) fn generate_into_json_value_fn( + &self, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> Option { + let variant_data_ident = &spec_gen_ctx.kubernetes_idents.parameter; + let variant_idents = &spec_gen_ctx.variant_idents; + + let serde_json_path = &*mod_gen_ctx.crates.serde_json; + + Some(quote! { + 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)?),)* + } + } + }) + } + + fn generate_conversion_match_arms( + &self, + versions: &[VersionDefinition], + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> Vec { + let variant_data_ident = &spec_gen_ctx.kubernetes_idents.parameter; + let version_enum_ident = &spec_gen_ctx.kubernetes_idents.version; + let struct_ident = &spec_gen_ctx.kubernetes_idents.kind; + + let versioned_path = &*mod_gen_ctx.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_api_version_string = format!( + "{group}/{desired_object_version_string}", + group = spec_gen_ctx.kubernetes_arguments.group + ); + let desired_object_variant_ident = &desired_object_version.idents.variant; + + 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(); + let converted: #module_ident::#struct_ident = #variant_data_ident.into(); + } + } else { + quote! { + // let converted: #module_ident::#spec_ident = converted.into(); + let converted: #module_ident::#struct_ident = converted.into(); + } + } + }); + + let kind = spec_gen_ctx.kubernetes_idents.kind.to_string(); + let steps = path.len(); + + let convert_object_trace = mod_gen_ctx.kubernetes_options.enable_tracing.is_present().then(|| quote! { + ::tracing::trace!( + #DESIRED_API_VERSION_ATTRIBUTE = #desired_object_api_version_string, + #API_VERSION_ATTRIBUTE = #current_object_version_string, + #STEPS_ATTRIBUTE = #steps, + #KIND_ATTRIBUTE = #kind, + "Successfully converted object" + ); + }); + + + quote! { + (Self::#current_object_version_ident(#variant_data_ident), #version_enum_ident::#desired_object_variant_ident) => { + #(#conversions)* + + let desired_object = Self::#desired_object_variant_ident(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_conversion_tracing( + &self, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> TracingTokens { + if mod_gen_ctx.kubernetes_options.enable_tracing.is_present() { + // TODO (@Techassi): Make tracing path configurable. Currently not possible, needs + // upstream change + let kind = spec_gen_ctx.kubernetes_idents.kind.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() + } + } +} + +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/k8s.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs deleted file mode 100644 index 7023cca0c..000000000 --- a/crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs +++ /dev/null @@ -1,742 +0,0 @@ -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; - let enum_ident_string = enum_ident.to_string(); - - // 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(kubernetes_arguments, 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_version_str()) - } - - #convert_method - - fn from_json_object(object_value: #serde_json_path::Value) -> ::std::result::Result { - let object_kind = object_value - .get("kind") - .ok_or_else(|| #parse_object_error::FieldMissing{ field: "kind".to_owned() })? - .as_str() - .ok_or_else(|| #parse_object_error::FieldNotStr{ field: "kind".to_owned() })?; - - // Note(@sbernauer): The kind must be checked here, because it is - // possible for the wrong object to be deserialized. - // Checking here stops us assuming the kind is correct and - // accidentally updating upgrade/downgrade information in the - // status in a later step. - if object_kind != #enum_ident_string { - return Err(#parse_object_error::UnexpectedKind{ - kind: object_kind.to_owned(), - expected: #enum_ident_string.to_owned(), - }); - } - - let api_version = object_value - .get("apiVersion") - .ok_or_else(|| #parse_object_error::FieldMissing{ field: "apiVersion".to_owned() })? - .as_str() - .ok_or_else(|| #parse_object_error::FieldNotStr{ field: "apiVersion".to_owned() })?; - - let object = match api_version { - #(#api_versions => { - let object = #serde_json_path::from_value(object_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, - kubernetes_arguments: &KubernetesArguments, - 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]}); - - let versioned_path = &*kubernetes_arguments.crates.versioned; - let unknown_desired_api_version_error = - quote! { #versioned_path::UnknownDesiredApiVersionError }; - - // 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 api_versions = variant_strings - .iter() - .map(|version| format!("{group}/{version}", group = &kubernetes_arguments.group)) - .collect::>(); - - quote! { - #automatically_derived - #[derive(Copy, Clone, Debug)] - #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> { - // The version (without the Kubernetes group) is probably more human-readable - f.write_str(self.as_version_str()) - } - } - - #automatically_derived - impl #enum_ident { - pub fn as_version_str(&self) -> &str { - match self { - #(#enum_ident::#variant_idents => #variant_strings),* - } - } - - pub fn as_api_version_str(&self) -> &str { - match self { - #(#enum_ident::#variant_idents => #api_versions),* - } - } - - pub fn from_api_version(api_version: &str) -> Result { - match api_version { - #(#api_versions => Ok(#enum_ident::#variant_idents)),*, - _ => Err(#unknown_desired_api_version_error { - api_version: api_version.to_owned(), - }), - } - } - } - } - } - - ///////////////////////// - // 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 version_enum_ident = &self.common.idents.kubernetes_version; - - 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 conversion_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() - } - }; - - // Convert all objects into the desired version - let response = match Self::convert_objects(request.objects, &request.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 desired_api_version = #version_enum_ident::from_api_version(desired_api_version) - .map_err(|source| #convert_object_error::ParseDesiredApiVersion { source })?; - - 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_object(object.clone()) - .map_err(|source| #convert_object_error::Parse { source })?; - - match (current_object, desired_api_version) { - #(#conversion_match_arms,)* - // In case the desired version matches the current object api version, we - // don't need to do anything. - // - // NOTE (@Techassi): I'm curious if this will ever happen? In theory the K8s - // apiserver should never send such a conversion review. - // - // Note(@sbernauer): I would prefer to explicitly list the remaining no-op - // cases, so the compiler ensures we did not miss a conversion - // // let version_idents = versions.iter().map(|v| &v.idents.variant); - // #( - // (Self::#version_idents(_), #version_enum_ident::#version_idents) - // )|* => converted_objects.push(object) - _ => converted_objects.push(object), - } - } - - ::std::result::Result::Ok(converted_objects) - } - }) - } - - fn generate_kubernetes_conversion_match_arms( - &self, - versions: &[VersionDefinition], - kubernetes_arguments: &KubernetesArguments, - ) -> Vec { - let group = &kubernetes_arguments.group; - let variant_data_ident = &self.common.idents.kubernetes_parameter; - let struct_ident = &self.common.idents.kubernetes; - let version_enum_ident = &self.common.idents.kubernetes_version; - 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_api_version_string = format!( - "{group}/{desired_object_version}", - desired_object_version = desired_object_version.inner - ); - 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_api_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), #version_enum_ident::#desired_object_variant_ident) => { - #(#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/merge.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/merge.rs new file mode 100644 index 000000000..926695a83 --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/merge.rs @@ -0,0 +1,40 @@ +use proc_macro2::TokenStream; +use quote::quote; + +use crate::codegen::container::{ + ModuleGenerationContext, + r#struct::{SpecGenerationContext, Struct}, +}; + +impl Struct { + pub(super) fn generate_merged_crd_fn( + &self, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> Option { + if mod_gen_ctx.skip.merged_crd.is_present() || self.common.options.skip_merged_crd { + return None; + } + + // Get various idents needed for code generation + let version_enum_ident = &spec_gen_ctx.kubernetes_idents.version; + + // Get the crate paths + let k8s_openapi_path = &*mod_gen_ctx.crates.k8s_openapi; + let kube_core_path = &*mod_gen_ctx.crates.kube_core; + + let crd_fns = &spec_gen_ctx.crd_fns; + + Some(quote! { + /// 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()) + } + }) + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs index ae13fe21c..67fd7dd93 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/mod.rs @@ -1,76 +1,31 @@ use std::ops::Not; -use darling::{Error, FromAttributes, Result}; +use darling::{Error, FromAttributes, Result, util::IdentString}; +use itertools::Itertools; use proc_macro2::TokenStream; use quote::quote; use syn::{Generics, ItemStruct}; use crate::{ - attrs::container::NestedContainerAttributes, + attrs::container::{ContainerAttributes, StructCrdArguments}, codegen::{ - ItemStatus, StandaloneContainerAttributes, VersionDefinition, + ItemStatus, VersionDefinition, changes::Neighbors, - container::{CommonContainerData, Container, ContainerIdents, ContainerOptions}, + container::{ + CommonContainerData, Container, ContainerIdents, ContainerOptions, ContainerTokens, + Direction, ExtendContainerTokens as _, KubernetesIdents, ModuleGenerationContext, + VersionContext, + }, item::VersionedField, }, }; -mod k8s; +mod conversion; +mod merge; impl Container { - pub fn new_standalone_struct( - item_struct: ItemStruct, - attributes: StandaloneContainerAttributes, - versions: &[VersionDefinition], - ) -> Result { - // NOTE (@Techassi): Should we check if the fields are named here? - let mut versioned_fields = Vec::new(); - - for field in item_struct.fields { - let mut versioned_field = VersionedField::new(field, versions)?; - versioned_field.insert_container_versions(versions); - versioned_fields.push(versioned_field); - } - - let kubernetes_arguments = attributes.kubernetes_arguments; - let idents = ContainerIdents::from(item_struct.ident, kubernetes_arguments.as_ref()); - - // Validate K8s specific requirements - // Ensure that the struct name includes the 'Spec' suffix. - if kubernetes_arguments.is_some() && !idents.original.as_str().ends_with("Spec") { - return Err(Error::custom( - "struct name needs to include the `Spec` suffix if Kubernetes features are enabled via `#[versioned(k8s())]`" - ).with_span(&idents.original.span())); - } - - let options = ContainerOptions { - skip_from: attributes - .common - .options - .skip - .is_some_and(|s| s.from.is_present()), - kubernetes_arguments, - }; - - let common = CommonContainerData { - original_attributes: item_struct.attrs, - options, - idents, - }; - - Ok(Self::Struct(Struct { - generics: item_struct.generics, - fields: versioned_fields, - common, - })) - } - - // TODO (@Techassi): See what can be unified into a single 'new' function - pub fn new_struct_nested( - item_struct: ItemStruct, - versions: &[VersionDefinition], - ) -> Result { - let attributes = NestedContainerAttributes::from_attributes(&item_struct.attrs)?; + pub fn new_struct(item_struct: ItemStruct, versions: &[VersionDefinition]) -> Result { + let attributes = ContainerAttributes::from_attributes(&item_struct.attrs)?; let mut versioned_fields = Vec::new(); for field in item_struct.fields { @@ -79,20 +34,29 @@ impl Container { versioned_fields.push(versioned_field); } - let kubernetes_arguments = attributes.kubernetes_arguments; - let idents = ContainerIdents::from(item_struct.ident, kubernetes_arguments.as_ref()); + let idents = ContainerIdents::from(item_struct.ident); + + let kubernetes_data = attributes.crd_arguments.map(|arguments| { + let idents = KubernetesIdents::from(&idents.original, &arguments); + KubernetesData { + kubernetes_arguments: arguments, + kubernetes_idents: idents, + } + }); // Validate K8s specific requirements // Ensure that the struct name includes the 'Spec' suffix. - if kubernetes_arguments.is_some() && !idents.original.as_str().ends_with("Spec") { + if kubernetes_data.is_some() && !idents.original.as_str().ends_with("Spec") { return Err(Error::custom( "struct name needs to include the `Spec` suffix if Kubernetes features are enabled via `#[versioned(k8s())]`" ).with_span(&idents.original.span())); } let options = ContainerOptions { - skip_from: attributes.options.skip.is_some_and(|s| s.from.is_present()), - kubernetes_arguments, + skip_from: attributes.skip.from.is_present(), + skip_object_from: attributes.skip.object_from.is_present(), + skip_merged_crd: attributes.skip.merged_crd.is_present(), + skip_try_convert: attributes.skip.try_convert.is_present(), }; // Nested structs @@ -113,6 +77,7 @@ impl Container { Ok(Self::Struct(Struct { generics: item_struct.generics, + kubernetes_data, fields: versioned_fields, common, })) @@ -128,28 +93,109 @@ pub struct Struct { /// Common container data which is shared between structs and enums. pub common: CommonContainerData, + pub kubernetes_data: Option, + /// Generic types of the struct pub generics: Generics, } +pub struct KubernetesData { + pub kubernetes_arguments: StructCrdArguments, + pub kubernetes_idents: KubernetesIdents, +} + // Common token generation impl Struct { + pub fn generate_tokens<'a>( + &self, + versions: &'a [VersionDefinition], + mod_gen_ctx: ModuleGenerationContext<'_>, + ) -> ContainerTokens<'a> { + let mut versions_iter = versions.iter().peekable(); + let mut container_tokens = ContainerTokens::default(); + + let spec_gen_ctx = + SpecGenerationContext::new(self.kubernetes_data.as_ref(), versions, mod_gen_ctx); + + while let Some(version) = versions_iter.next() { + let next_version = versions_iter.peek().copied(); + let ver_ctx = VersionContext::new(version, next_version); + + let struct_definition = + self.generate_definition(ver_ctx, mod_gen_ctx, spec_gen_ctx.as_ref()); + + let upgrade_from = self.generate_from_impl(Direction::Upgrade, ver_ctx, mod_gen_ctx); + let downgrade_from = + self.generate_from_impl(Direction::Downgrade, ver_ctx, mod_gen_ctx); + + // Generate code which is only needed for the top-level CRD spec + if let Some(spec_gen_ctx) = &spec_gen_ctx { + let upgrade_spec_from = self.generate_object_from_impl( + Direction::Upgrade, + ver_ctx, + mod_gen_ctx, + spec_gen_ctx, + ); + + let downgrade_spec_from = self.generate_object_from_impl( + Direction::Downgrade, + ver_ctx, + mod_gen_ctx, + spec_gen_ctx, + ); + + container_tokens + .extend_between(&version.inner, upgrade_spec_from) + .extend_between(&version.inner, downgrade_spec_from); + } + + container_tokens + .extend_inner(&version.inner, struct_definition) + .extend_between(&version.inner, upgrade_from) + .extend_between(&version.inner, downgrade_from); + } + + // Generate code which is only needed for the top-level CRD spec + if let Some(spec_gen_ctx) = spec_gen_ctx { + let entry_enum = self.generate_entry_enum(mod_gen_ctx, &spec_gen_ctx); + let entry_enum_impl = + self.generate_entry_impl_block(versions, mod_gen_ctx, &spec_gen_ctx); + let version_enum = self.generate_version_enum(mod_gen_ctx, &spec_gen_ctx); + let status_struct = self.generate_status_struct(mod_gen_ctx, &spec_gen_ctx); + + container_tokens + .extend_outer(entry_enum) + .extend_outer(entry_enum_impl) + .extend_outer(version_enum) + .extend_outer(status_struct); + } + + container_tokens + } + /// Generates code for the struct definition. - pub fn generate_definition(&self, version: &VersionDefinition) -> TokenStream { + fn generate_definition( + &self, + ver_ctx: VersionContext<'_>, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: Option<&SpecGenerationContext<'_>>, + ) -> TokenStream { let where_clause = self.generics.where_clause.as_ref(); let type_generics = &self.generics; let original_attributes = &self.common.original_attributes; let ident = &self.common.idents.original; - let version_docs = &version.docs; + let version_docs = &ver_ctx.version.docs; - let mut fields = TokenStream::new(); - for field in &self.fields { - fields.extend(field.generate_for_container(version)); - } + let fields: TokenStream = self + .fields + .iter() + .filter_map(|field| field.generate_for_container(ver_ctx.version)) + .collect(); - // This only returns Some, if K8s features are enabled - let kube_attribute = self.generate_kube_attribute(version); + let kube_attribute = spec_gen_ctx.and_then(|spec_gen_ctx| { + self.generate_kube_attribute(ver_ctx, mod_gen_ctx, spec_gen_ctx) + }); quote! { #(#[doc = #version_docs])* @@ -161,135 +207,348 @@ impl Struct { } } - // TODO (@Techassi): It looks like some of the stuff from the upgrade and downgrade functions - // can be combined into a single piece of code. Figure out a nice way to do that. - /// Generates code for the `From for NextVersion` implementation. - /// - /// The `add_attributes` parameter declares if attributes (macros) should be added to the - /// generated `From` impl block. - pub fn generate_upgrade_from_impl( + fn generate_kube_attribute( &self, - version: &VersionDefinition, - next_version: Option<&VersionDefinition>, - add_attributes: bool, + ver_ctx: VersionContext<'_>, + gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, ) -> Option { - if version.skip_from || self.common.options.skip_from { - return None; + // Required arguments + let group = &spec_gen_ctx.kubernetes_arguments.group; + let version = ver_ctx.version.inner.to_string(); + let kind = spec_gen_ctx + .kubernetes_arguments + .kind + .as_ref() + .map_or(spec_gen_ctx.kubernetes_idents.kind.to_string(), |kind| { + kind.clone() + }); + + // Optional arguments + let singular = spec_gen_ctx + .kubernetes_arguments + .singular + .as_ref() + .map(|s| quote! { , singular = #s }); + + let plural = spec_gen_ctx + .kubernetes_arguments + .plural + .as_ref() + .map(|p| quote! { , plural = #p }); + + let crates = gen_ctx.crates; + + let namespaced = spec_gen_ctx + .kubernetes_arguments + .namespaced + .is_present() + .then_some(quote! { , namespaced }); + + let status = match ( + gen_ctx + .kubernetes_options + .experimental_conversion_tracking + .is_present(), + &spec_gen_ctx.kubernetes_arguments.status, + ) { + (true, _) => { + let status_ident = &spec_gen_ctx.kubernetes_idents.status; + Some(quote! { , status = #status_ident }) + } + (_, Some(status_ident)) => Some(quote! { , status = #status_ident }), + (_, _) => None, + }; + + let shortnames: TokenStream = spec_gen_ctx + .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 + )] + }) + } + + fn generate_entry_enum( + &self, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> TokenStream { + let enum_ident = &spec_gen_ctx.kubernetes_idents.kind; + let vis = mod_gen_ctx.vis; + + let variant_idents = &spec_gen_ctx.variant_idents; + let variant_data = &spec_gen_ctx.variant_data; + + quote! { + #[derive(Debug)] + #vis enum #enum_ident { + #(#variant_idents(#variant_data)),* + } } + } - match next_version { - Some(next_version) => { - // TODO (@Techassi): Support generic types which have been removed in newer versions, - // but need to exist for older versions How do we represent that? Because the - // defined struct always represents the latest version. I guess we could generally - // 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 for_module_ident = &next_version.idents.module; - let from_module_ident = &version.idents.module; - - let fields: TokenStream = self - .fields - .iter() - .map(|f| { - f.generate_for_upgrade_from_impl(version, next_version, from_struct_ident) - }) - .collect(); - - // Include allow(deprecated) only when this or the next version is - // deprecated. Also include it, when a field in this or the next - // version is deprecated. - let allow_attribute = (version.deprecated.is_some() - || next_version.deprecated.is_some() - || self.is_any_field_deprecated(version) - || self.is_any_field_deprecated(next_version)) - .then(|| quote! { #[allow(deprecated)] }); - - // Only add the #[automatically_derived] attribute only if this impl is used - // outside of a module (in standalone mode). - let automatically_derived = add_attributes - .not() - .then(|| quote! {#[automatically_derived]}); - - Some(quote! { - #automatically_derived - #allow_attribute - impl #impl_generics ::std::convert::From<#from_module_ident::#struct_ident #type_generics> for #for_module_ident::#struct_ident #type_generics - #where_clause - { - fn from(#from_struct_ident: #from_module_ident::#struct_ident #type_generics) -> Self { - Self { - #fields - } - } - } - }) + fn generate_entry_impl_block( + &self, + versions: &[VersionDefinition], + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> TokenStream { + let enum_ident = &spec_gen_ctx.kubernetes_idents.kind; + + // Only generate merged_crd associated function if not opted out + let merged_crd_fn = + if !mod_gen_ctx.skip.merged_crd.is_present() && !self.common.options.skip_merged_crd { + Some(self.generate_merged_crd_fn(mod_gen_ctx, spec_gen_ctx)) + } else { + None + }; + + let try_convert_fn = self.generate_try_convert_fn(versions, mod_gen_ctx, spec_gen_ctx); + let from_json_value_fn = self.generate_from_json_value_fn(mod_gen_ctx, spec_gen_ctx); + let into_json_value_fn = self.generate_into_json_value_fn(mod_gen_ctx, spec_gen_ctx); + + quote! { + impl #enum_ident { + #merged_crd_fn + #try_convert_fn + #from_json_value_fn + #into_json_value_fn } - None => None, } } - pub fn generate_downgrade_from_impl( + fn generate_version_enum( &self, - version: &VersionDefinition, - next_version: Option<&VersionDefinition>, - add_attributes: bool, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, ) -> Option { - if version.skip_from || self.common.options.skip_from { + if (mod_gen_ctx.skip.merged_crd.is_present() || self.common.options.skip_merged_crd) + && (mod_gen_ctx.skip.try_convert.is_present() || self.common.options.skip_try_convert) + { return None; } - 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 enum_ident = &spec_gen_ctx.kubernetes_idents.version; + let vis = mod_gen_ctx.vis; + + let version_strings = &spec_gen_ctx.version_strings; + let variant_idents = &spec_gen_ctx.variant_idents; - let from_module_ident = &next_version.idents.module; - let for_module_ident = &version.idents.module; + Some(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 { + #(#enum_ident::#variant_idents => #version_strings),* + } + } + } + }) + } - let fields: TokenStream = self + // Generates the Kubernetes specific From impl for all structs which are part of a spec. + pub fn generate_from_impl( + &self, + direction: Direction, + ver_ctx: VersionContext<'_>, + mod_gen_ctx: ModuleGenerationContext<'_>, + ) -> Option { + if mod_gen_ctx.skip.from.is_present() || self.common.options.skip_from { + return None; + } + + let next_version = ver_ctx.next_version; + let version = ver_ctx.version; + + // TODO (@Techassi): The crate overrides need to be applied to the module instead and must + // be disallowed on individual structs. + next_version.map(|next_version| { + // TODO (@Techassi): Support generic types which have been removed in newer versions, + // but need to exist for older versions How do we represent that? Because the + // defined struct always represents the latest version. I guess we could generally + // 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 version_string = version.inner.to_string(); + + let versioned_path = &*mod_gen_ctx.crates.versioned; + + // Include allow(deprecated) only when this or the next version is + // deprecated. Also include it, when a field in this or the next + // version is deprecated. + let allow_attribute = (version.deprecated.is_some() + || next_version.deprecated.is_some() + || self.is_any_field_deprecated(version) + || self.is_any_field_deprecated(next_version)) + .then(|| quote! { #[allow(deprecated)] }); + + // Only add the #[automatically_derived] attribute only if this impl is used + // outside of a module (in standalone mode). + let automatically_derived = mod_gen_ctx.add_attributes + .not() + .then(|| quote! {#[automatically_derived]}); + + let fields = |direction: Direction| -> TokenStream { + self .fields .iter() - .map(|f| { - f.generate_for_downgrade_from_impl(version, next_version, from_struct_ident) + .filter_map(|f| { + f.generate_for_from_impl( + direction, + version, + next_version, + from_struct_ident, + ) }) - .collect(); - - // Include allow(deprecated) only when this or the next version is - // deprecated. Also include it, when a field in this or the next - // version is deprecated. - let allow_attribute = (version.deprecated.is_some() - || next_version.deprecated.is_some() - || self.is_any_field_deprecated(version) - || self.is_any_field_deprecated(next_version)) - .then(|| quote! { #[allow(deprecated)] }); - - // Only add the #[automatically_derived] attribute only if this impl is used - // outside of a module (in standalone mode). - let automatically_derived = add_attributes - .not() - .then(|| quote! {#[automatically_derived]}); - - Some(quote! { - #automatically_derived - #allow_attribute - impl #impl_generics ::std::convert::From<#from_module_ident::#struct_ident #type_generics> for #for_module_ident::#struct_ident #type_generics - #where_clause - { - fn from(#from_struct_ident: #from_module_ident::#struct_ident #type_generics) -> Self { - Self { - #fields - } - } + .collect() + }; + + let inserts: TokenStream = self.fields.iter().filter_map(|f| { + f.generate_for_status_insertion(direction, next_version, from_struct_ident, mod_gen_ctx) + }).collect(); + + let (fields, for_module_ident, from_module_ident) = match direction { + Direction::Upgrade => { + let from_module_ident = &version.idents.module; + let for_module_ident = &next_version.idents.module; + + (fields(Direction::Upgrade), for_module_ident, from_module_ident) + } + Direction::Downgrade => { + let from_module_ident = &next_version.idents.module; + let for_module_ident = &version.idents.module; + + (fields(Direction::Downgrade), for_module_ident, from_module_ident) + } + }; + + // TODO (@Techassi): Re-add support for generics + // TODO (@Techassi): We know the status, so we can hard-code it, but hard to track across structs + + quote! { + #automatically_derived + #allow_attribute + impl #versioned_path::TrackingFrom<#from_module_ident::#struct_ident, S> for #for_module_ident::#struct_ident + where + S: #versioned_path::TrackingStatus + ::core::default::Default + { + // TODO (@Techassi): Figure out how we can set the correct parent here. Maybe + // a map from field name to type and where the type matches _this_ ident. + const PARENT: Option<&str> = None; + + #[allow(unused)] + fn tracking_from(#from_struct_ident: #from_module_ident::#struct_ident, status: &S) -> Self { + // TODO (@Techassi): Depending on the direction, we need to either insert + // changed values into the upgrade or downgrade section. Only then we can + // convert the spec. + + // FIXME (@Techassi): We shouldn't create an entry if we don't need to. This + // currently pollutes the status. + // TODO (@Techassi): Change the key from a Version to a String to avoid + // parsing the version. We know the version is valid, because we previously + // parsed it via this macro. + let entry = status + .changes() + .upgrades + .entry(#version_string.parse().unwrap()) + .or_default(); + + #inserts + + let spec = Self { + #fields + }; + + // After the spec is converted, depending on the direction, we need to apply + // changed values from either the upgrade or downgrade section. Afterwards + // we can return the successfully converted spec and the status contains + // the tracked changes. + + spec } - }) + } } - None => None, + }) + } + + // Generates the Kubernetes specific From impl for the top-level object. + pub fn generate_object_from_impl( + &self, + direction: Direction, + ver_ctx: VersionContext<'_>, + mod_gen_ctx: ModuleGenerationContext<'_>, + spec_gen_ctx: &SpecGenerationContext<'_>, + ) -> Option { + if mod_gen_ctx.skip.object_from.is_present() || self.common.options.skip_object_from { + return None; } + + let next_version = ver_ctx.next_version; + let version = ver_ctx.version; + + next_version.map(|next_version| { + let from_struct_parameter_ident = &spec_gen_ctx.kubernetes_idents.parameter; + let object_struct_ident = &spec_gen_ctx.kubernetes_idents.kind; + let spec_struct_ident = &self.common.idents.original; + + let versioned_path = &*mod_gen_ctx.crates.versioned; + + let (for_module_ident, from_module_ident) = match direction { + Direction::Upgrade => (&next_version.idents.module, &version.idents.module), + Direction::Downgrade => (&version.idents.module, &next_version.idents.module), + }; + + quote! { + impl ::std::convert::From<#from_module_ident::#object_struct_ident> for #for_module_ident::#object_struct_ident { + fn from(#from_struct_parameter_ident: #from_module_ident::#object_struct_ident) -> Self { + // The status is optional. The be able to track changes in nested sub structs it needs + // to be initialized with a default value. + let mut status = #from_struct_parameter_ident.status.unwrap_or_default(); + + // Convert the spec and track values in the status + let spec = + <#for_module_ident::#spec_struct_ident as #versioned_path::TrackingFrom<_, _>>::tracking_from( + #from_struct_parameter_ident.spec, + &status + ); + + // Construct the final object by copying over the metadata, setting the status and + // using the converted spec. + Self { + metadata: #from_struct_parameter_ident.metadata, + status: Some(status), + spec, + } + } + } + } + }) } /// Returns whether any field is deprecated in the provided `version`. @@ -315,3 +574,65 @@ impl Struct { }) } } + +#[derive(Debug)] +pub struct SpecGenerationContext<'a> { + pub kubernetes_arguments: &'a StructCrdArguments, + pub kubernetes_idents: &'a KubernetesIdents, + + pub crd_fns: Vec, + pub variant_idents: Vec, + pub variant_data: Vec, + pub version_strings: Vec, +} + +impl<'a> SpecGenerationContext<'a> { + pub fn new( + data: Option<&'a KubernetesData>, + versions: &[VersionDefinition], + mod_gen_ctx: ModuleGenerationContext<'_>, + ) -> Option { + match data { + Some(KubernetesData { + kubernetes_arguments, + kubernetes_idents, + }) => { + let (crd_fns, variant_idents, variant_data, version_strings) = versions + .iter() + .map(|version| { + Self::generate_version_items(version, mod_gen_ctx, &kubernetes_idents.kind) + }) + .multiunzip::<(Vec<_>, Vec<_>, Vec<_>, Vec<_>)>(); + + Some(Self { + kubernetes_arguments, + kubernetes_idents, + crd_fns, + variant_idents, + variant_data, + version_strings, + }) + } + None => None, + } + } + + fn generate_version_items( + version: &VersionDefinition, + mod_gen_ctx: ModuleGenerationContext<'_>, + struct_ident: &IdentString, + ) -> (TokenStream, IdentString, TokenStream, String) { + let module_ident = &version.idents.module; + + let kube_core_path = &*mod_gen_ctx.crates.kube_core; + + let variant_data = quote! { #module_ident::#struct_ident }; + let crd_fn = quote! { + <#module_ident::#struct_ident as #kube_core_path::CustomResourceExt>::crd() + }; + let variant_ident = version.idents.variant.clone(); + let version_string = version.inner.to_string(); + + (crd_fn, variant_ident, variant_data, version_string) + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/item/field.rs b/crates/stackable-versioned-macros/src/codegen/item/field.rs index 358a94000..46f24c267 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/field.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/field.rs @@ -11,6 +11,7 @@ use crate::{ codegen::{ ItemStatus, VersionDefinition, changes::{BTreeMapExt, ChangesetExt}, + container::{Direction, ModuleGenerationContext}, }, utils::FieldIdent, }; @@ -140,13 +141,13 @@ impl VersionedField { } } - // TODO (@Techassi): This should probably return an optional token stream. - pub fn generate_for_upgrade_from_impl( + pub fn generate_for_from_impl( &self, + direction: Direction, version: &VersionDefinition, next_version: &VersionDefinition, from_struct_ident: &IdentString, - ) -> TokenStream { + ) -> Option { match &self.changes { Some(changes) => { let next_change = changes.get_expect(&next_version.inner); @@ -156,35 +157,45 @@ impl VersionedField { // If both this status and the next one is NotPresent, which means // a field was introduced after a bunch of versions, we don't // need to generate any code for the From impl. - (ItemStatus::NotPresent, ItemStatus::NotPresent) => { - quote! {} - } + (ItemStatus::NotPresent, ItemStatus::NotPresent) => None, ( _, ItemStatus::Addition { ident, default_fn, .. }, - ) => quote! { - #ident: #default_fn(), + ) => match direction { + Direction::Upgrade => Some(quote! { #ident: #default_fn(), }), + Direction::Downgrade => None, }, ( _, ItemStatus::Change { - from_ident: old_field_ident, + downgrade_with, upgrade_with, + from_ident, to_ident, .. }, - ) => match upgrade_with { - // The user specified a custom conversion function which - // will be used here instead of the default .into() call - // which utilizes From impls. - Some(upgrade_fn) => quote! { - #to_ident: #upgrade_fn(#from_struct_ident.#old_field_ident), + ) => match direction { + Direction::Upgrade => match upgrade_with { + // The user specified a custom conversion function which + // will be used here instead of the default .into() call + // which utilizes From impls. + Some(upgrade_fn) => Some(quote! { + #to_ident: #upgrade_fn(#from_struct_ident.#from_ident), + }), + // Default .into() call using From impls. + None => Some(quote! { + #to_ident: #from_struct_ident.#from_ident.into(), + }), }, - // Default .into() call using From impls. - None => quote! { - #to_ident: #from_struct_ident.#old_field_ident.into(), + Direction::Downgrade => match downgrade_with { + Some(downgrade_fn) => Some(quote! { + #from_ident: #downgrade_fn(#from_struct_ident.#to_ident), + }), + None => Some(quote! { + #from_ident: #from_struct_ident.#to_ident.into(), + }), }, }, (old, next) => { @@ -194,8 +205,13 @@ impl VersionedField { // NOTE (@Techassi): Do we really need .into() here. I'm // currently not sure why it is there and if it is needed // in some edge cases. - quote! { - #next_field_ident: #from_struct_ident.#old_field_ident.into(), + match direction { + Direction::Upgrade => Some(quote! { + #next_field_ident: #from_struct_ident.#old_field_ident.into(), + }), + Direction::Downgrade => Some(quote! { + #old_field_ident: #from_struct_ident.#next_field_ident.into(), + }), } } } @@ -203,68 +219,51 @@ impl VersionedField { None => { let field_ident = &*self.ident; - quote! { + Some(quote! { #field_ident: #from_struct_ident.#field_ident.into(), - } + }) } } } - pub fn generate_for_downgrade_from_impl( + pub fn generate_for_status_insertion( &self, - version: &VersionDefinition, + direction: Direction, next_version: &VersionDefinition, from_struct_ident: &IdentString, - ) -> TokenStream { + mod_gen_ctx: ModuleGenerationContext<'_>, + ) -> Option { match &self.changes { - Some(changes) => { - let next_change = changes.get_expect(&next_version.inner); - let change = changes.get_expect(&version.inner); + Some(changes) => match direction { + // This arm is only relevant for removed fields which are currently + // not supported. + Direction::Upgrade => None, - match (change, next_change) { - // If both this status and the next one is NotPresent, which means - // a field was introduced after a bunch of versions, we don't - // need to generate any code for the From impl. - (ItemStatus::NotPresent, ItemStatus::NotPresent) => { - quote! {} - } - (_, ItemStatus::Addition { .. }) => quote! {}, - ( - _, - ItemStatus::Change { - downgrade_with, - from_ident: old_field_ident, - to_ident, - .. - }, - ) => match downgrade_with { - Some(downgrade_fn) => quote! { - #old_field_ident: #downgrade_fn(#from_struct_ident.#to_ident), - }, - None => quote! { - #old_field_ident: #from_struct_ident.#to_ident.into(), - }, - }, - (old, next) => { - let next_field_ident = next.get_ident(); - let old_field_ident = old.get_ident(); + // When we generate code for a downgrade, any changes which need to + // be tracked need to be inserted into the upgrade section for the + // next time an upgrade needs to be done. + Direction::Downgrade => { + let next_change = changes.get_expect(&next_version.inner); - // NOTE (@Techassi): Do we really need .into() here. I'm - // currently not sure why it is there and if it is needed - // in some edge cases. - quote! { - #old_field_ident: #from_struct_ident.#next_field_ident.into(), + let serde_yaml_path = &*mod_gen_ctx.crates.serde_yaml; + let versioned_path = &*mod_gen_ctx.crates.versioned; + + match next_change { + ItemStatus::Addition { ident, .. } => { + let ident_str = ident.as_str(); + + Some(quote! { + entry.push(#versioned_path::ChangedValue { + field_name: #ident_str.to_owned(), + value: #serde_yaml_path::to_value(&#from_struct_ident.#ident).unwrap(), + }); + }) } + _ => None, } } - } - None => { - let field_ident = &*self.ident; - - quote! { - #field_ident: #from_struct_ident.#field_ident.into(), - } - } + }, + None => None, } } } diff --git a/crates/stackable-versioned-macros/src/codegen/item/variant.rs b/crates/stackable-versioned-macros/src/codegen/item/variant.rs index b9c30f750..30dd188d8 100644 --- a/crates/stackable-versioned-macros/src/codegen/item/variant.rs +++ b/crates/stackable-versioned-macros/src/codegen/item/variant.rs @@ -11,6 +11,7 @@ use crate::{ codegen::{ ItemStatus, VersionDefinition, changes::{BTreeMapExt, ChangesetExt}, + container::Direction, }, utils::VariantIdent, }; @@ -130,8 +131,9 @@ impl VersionedVariant { } } - pub fn generate_for_upgrade_from_impl( + pub fn generate_for_from_impl( &self, + direction: Direction, version: &VersionDefinition, next_version: &VersionDefinition, enum_ident: &IdentString, @@ -159,9 +161,10 @@ impl VersionedVariant { #next_version_ident::#enum_ident::#next_variant_ident #variant_fields }; - Some(quote! { - #old => #next, - }) + match direction { + Direction::Upgrade => Some(quote! { #old => #next, }), + Direction::Downgrade => Some(quote! { #next => #old, }), + } } } } @@ -177,64 +180,11 @@ impl VersionedVariant { #next_version_ident::#enum_ident::#variant_ident #variant_fields }; - Some(quote! { - #old => #next, - }) - } - } - } - - pub fn generate_for_downgrade_from_impl( - &self, - version: &VersionDefinition, - next_version: &VersionDefinition, - enum_ident: &IdentString, - ) -> Option { - let variant_fields = self.fields_as_token_stream(); - - match &self.changes { - Some(changes) => { - let next_change = changes.get_expect(&next_version.inner); - let change = changes.get_expect(&version.inner); - - match (change, next_change) { - (_, ItemStatus::Addition { .. }) => None, - (old, next) => { - 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(); - - let old = quote! { - #old_version_ident::#enum_ident::#old_variant_ident #variant_fields - }; - let next = quote! { - #next_version_ident::#enum_ident::#next_variant_ident #variant_fields - }; - - Some(quote! { - #next => #old, - }) - } + match direction { + Direction::Upgrade => Some(quote! { #old => #next, }), + Direction::Downgrade => Some(quote! { #next => #old, }), } } - None => { - let next_version_ident = &next_version.idents.module; - let old_version_ident = &version.idents.module; - let variant_ident = &*self.ident; - - let old = quote! { - #old_version_ident::#enum_ident::#variant_ident #variant_fields - }; - let next = quote! { - #next_version_ident::#enum_ident::#variant_ident #variant_fields - }; - - Some(quote! { - #next => #old, - }) - } } } diff --git a/crates/stackable-versioned-macros/src/codegen/mod.rs b/crates/stackable-versioned-macros/src/codegen/mod.rs index 6c39b2e51..4dd91163c 100644 --- a/crates/stackable-versioned-macros/src/codegen/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/mod.rs @@ -4,7 +4,7 @@ use proc_macro2::TokenStream; use syn::{Path, Type}; use crate::{ - attrs::{container::StandaloneContainerAttributes, module::ModuleAttributes}, + attrs::module::ModuleAttributes, utils::{VersionExt, doc_comments::DocComments}, }; @@ -43,35 +43,9 @@ impl Ord for VersionDefinition { } } -// NOTE (@Techassi): Can we maybe unify these two impls? -impl From<&StandaloneContainerAttributes> for Vec { - fn from(attributes: &StandaloneContainerAttributes) -> Self { - attributes - .common - .versions - .iter() - .map(|v| VersionDefinition { - skip_from: v.skip.as_ref().is_some_and(|s| s.from.is_present()), - 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: v.doc.as_deref().into_doc_comments(), - inner: v.name, - }) - .collect() - } -} - impl From<&ModuleAttributes> for Vec { fn from(attributes: &ModuleAttributes) -> Self { attributes - .common .versions .iter() .map(|v| VersionDefinition { diff --git a/crates/stackable-versioned-macros/src/codegen/module.rs b/crates/stackable-versioned-macros/src/codegen/module.rs index c801de4a3..f865d53bb 100644 --- a/crates/stackable-versioned-macros/src/codegen/module.rs +++ b/crates/stackable-versioned-macros/src/codegen/module.rs @@ -3,11 +3,17 @@ use std::{collections::HashMap, ops::Not}; use darling::{Error, Result, util::IdentString}; use proc_macro2::TokenStream; use quote::quote; -use syn::{Ident, Item, ItemMod, ItemUse, Visibility, token::Pub}; +use syn::{Item, ItemMod, ItemUse, Visibility, token::Pub}; use crate::{ ModuleAttributes, - codegen::{KubernetesTokens, VersionDefinition, container::Container}, + attrs::module::{CrateArguments, ModuleOptions, ModuleSkipArguments}, + codegen::{ + VersionDefinition, + container::{ + Container, ContainerTokens, ModuleGenerationContext, VersionedContainerTokens, + }, + }, }; /// A versioned module. @@ -24,9 +30,9 @@ pub struct Module { ident: IdentString, vis: Visibility, - // Flags which influence generation - preserve_module: bool, - skip_from: bool, + crates: CrateArguments, + options: ModuleOptions, + skip: ModuleSkipArguments, } impl Module { @@ -40,19 +46,6 @@ impl Module { let versions: Vec = (&module_attributes).into(); - let preserve_module = module_attributes - .common - .options - .preserve_module - .is_present(); - - let skip_from = module_attributes - .common - .options - .skip - .as_ref() - .is_some_and(|opts| opts.from.is_present()); - let mut errors = Error::accumulator(); let mut submodules = HashMap::new(); let mut containers = Vec::new(); @@ -60,12 +53,18 @@ impl Module { for item in items { match item { Item::Enum(item_enum) => { - let container = Container::new_enum_nested(item_enum, &versions)?; - containers.push(container); + if let Some(container) = + errors.handle(Container::new_enum(item_enum, &versions)) + { + containers.push(container); + }; } Item::Struct(item_struct) => { - let container = Container::new_struct_nested(item_struct, &versions)?; - containers.push(container); + if let Some(container) = + errors.handle(Container::new_struct(item_struct, &versions)) + { + containers.push(container); + } } Item::Mod(submodule) => { if !versions @@ -120,12 +119,13 @@ impl Module { } errors.finish_with(Self { + options: module_attributes.options, + crates: module_attributes.crates, + skip: module_attributes.skip, ident: item_mod.ident.into(), vis: item_mod.vis, - preserve_module, containers, submodules, - skip_from, versions, }) } @@ -136,6 +136,9 @@ impl Module { return quote! {}; } + let preserve_module = self.options.common.preserve_module.is_present(); + let allow_unsorted = self.options.common.allow_unsorted.is_present(); + let module_ident = &self.ident; let module_vis = &self.vis; @@ -143,61 +146,54 @@ impl Module { // of version modules (eg. 'v1alpha1') to be public, so that they are accessible inside the // preserved (wrapping) module. Otherwise, we can inherit the visibility from the module // which will be erased. - let version_module_vis = if self.preserve_module { + let version_module_vis = if allow_unsorted { &Visibility::Public(Pub::default()) } else { &self.vis }; - let mut kubernetes_tokens = TokenStream::new(); + let mut inner_and_between_tokens = HashMap::new(); + let mut outer_tokens = TokenStream::new(); let mut tokens = TokenStream::new(); - let mut kubernetes_container_items: HashMap = HashMap::new(); - let mut versions = self.versions.iter().peekable(); + let ctx = ModuleGenerationContext { + kubernetes_options: &self.options.kubernetes, + add_attributes: preserve_module, + vis: version_module_vis, + crates: &self.crates, + skip: &self.skip, + }; - while let Some(version) = versions.next() { - let next_version = versions.peek().copied(); - let mut container_definitions = TokenStream::new(); - let mut from_impls = TokenStream::new(); + for container in &self.containers { + let ContainerTokens { versioned, outer } = + container.generate_tokens(&self.versions, ctx); - let version_module_ident = &version.idents.module; + inner_and_between_tokens.insert(container.get_original_ident(), versioned); + outer_tokens.extend(outer); + } - for container in &self.containers { - container_definitions.extend(container.generate_definition(version)); - - if !self.skip_from { - from_impls.extend(container.generate_upgrade_from_impl( - version, - next_version, - self.preserve_module, - )); - - from_impls.extend(container.generate_downgrade_from_impl( - version, - next_version, - self.preserve_module, - )); - } + // Only add #[automatically_derived] here if the user doesn't want to preserve the + // module. + let automatically_derived = preserve_module + .not() + .then(|| quote! {#[automatically_derived]}); - // Generate Kubernetes specific code which is placed outside of the container - // definition. - if let Some(items) = container.generate_kubernetes_version_items(version) { - let entry = kubernetes_container_items - .entry(container.get_original_ident().clone()) - .or_default(); + for version in &self.versions { + let mut inner_tokens = TokenStream::new(); + let mut between_tokens = TokenStream::new(); - entry.push(items); - } + for container in &self.containers { + let versioned = inner_and_between_tokens + .get_mut(container.get_original_ident()) + .unwrap(); + let VersionedContainerTokens { inner, between } = + versioned.remove(&version.inner).unwrap(); + + inner_tokens.extend(inner); + between_tokens.extend(between); } - let submodule_imports = self.generate_submodule_imports(version); - - // Only add #[automatically_derived] here if the user doesn't want to preserve the - // module. - let automatically_derived = self - .preserve_module - .not() - .then(|| quote! {#[automatically_derived]}); + let version_module_ident = &version.idents.module; // Add the #[deprecated] attribute when the version is marked as deprecated. let deprecated_attribute = version @@ -205,6 +201,8 @@ impl Module { .as_ref() .map(|note| quote! { #[deprecated = #note] }); + let submodule_imports = self.generate_submodule_imports(version); + tokens.extend(quote! { #automatically_derived #deprecated_attribute @@ -212,39 +210,25 @@ impl Module { use super::*; #submodule_imports - - #container_definitions + #inner_tokens } - #from_impls + #between_tokens }); } - // 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(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, - )); - } - } - - if self.preserve_module { + if preserve_module { quote! { #[automatically_derived] #module_vis mod #module_ident { #tokens - #kubernetes_tokens + #outer_tokens } } } else { quote! { #tokens - #kubernetes_tokens + #outer_tokens } } } diff --git a/crates/stackable-versioned-macros/src/lib.rs b/crates/stackable-versioned-macros/src/lib.rs index a028be25e..0783a4429 100644 --- a/crates/stackable-versioned-macros/src/lib.rs +++ b/crates/stackable-versioned-macros/src/lib.rs @@ -2,10 +2,7 @@ use darling::{FromMeta, ast::NestedMeta}; use proc_macro::TokenStream; use syn::{Error, Item, spanned::Spanned}; -use crate::{ - attrs::{container::StandaloneContainerAttributes, module::ModuleAttributes}, - codegen::{container::StandaloneContainer, module::Module}, -}; +use crate::{attrs::module::ModuleAttributes, codegen::module::Module}; #[cfg(test)] mod test_utils; @@ -711,290 +708,285 @@ mod utils; /// } /// ``` /// -#[cfg_attr( - feature = "k8s", - doc = r#" -# Kubernetes-specific Features - -This macro also offers support for Kubernetes-specific versioning, -especially for CustomResourceDefinitions (CRDs). These features are -completely opt-in. You need to enable the `k8s` feature (which enables -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; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -#[versioned( - version(name = "v1alpha1"), - version(name = "v1beta1"), - version(name = "v1"), - k8s(group = "example.com") -)] -#[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] -pub struct FooSpec { - #[versioned( - added(since = "v1beta1"), - changed( - since = "v1", - from_name = "prev_bar", - from_type = "u16", - downgrade_with = usize_to_u16 - ) - )] - bar: usize, - baz: bool, -} - -fn usize_to_u16(input: usize) -> u16 { - input.try_into().unwrap() -} -# fn main() {} -``` - -## 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 -however be noted, that specifying Kubernetes specific arguments is done on the -container level instead of on the module level, which is detailed in the -following example: - -``` -# use stackable_versioned_macros::versioned; -# use kube::CustomResource; -# use schemars::JsonSchema; -# use serde::{Deserialize, Serialize}; -#[versioned( - version(name = "v1alpha1"), - version(name = "v1") -)] -mod versioned { - #[versioned(k8s(group = "foo.example.org"))] - #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] - struct FooSpec { - bar: usize, - } - - #[versioned(k8s(group = "bar.example.org"))] - #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] - struct BarSpec { - baz: String, - } -} -# fn main() {} -``` - -
-Expand Generated Code - -```ignore -mod v1alpha1 { - use super::*; - #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] - #[kube( - group = "foo.example.org", - version = "v1alpha1", - kind = "Foo" - )] - pub struct FooSpec { - pub bar: usize, - } - - #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] - #[kube( - group = "bar.example.org", - version = "v1alpha1", - kind = "Bar" - )] - pub struct BarSpec { - pub bar: usize, - } -} - -mod v1 { - use super::*; - #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] - #[kube( - group = "foo.example.org", - version = "v1", - kind = "Foo" - )] - pub struct FooSpec { - pub bar: usize, - } - - #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] - #[kube( - group = "bar.example.org", - version = "v1", - kind = "Bar" - )] - pub struct BarSpec { - pub bar: usize, - } -} -``` -
- -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 -"# -)] +/// # Kubernetes-specific Features +/// +/// This macro also offers support for Kubernetes-specific versioning, +/// especially for CustomResourceDefinitions (CRDs). These features are +/// completely opt-in. You need to enable the `k8s` feature (which enables +/// 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; +/// use schemars::JsonSchema; +/// use serde::{Deserialize, Serialize}; +/// +/// #[versioned( +/// version(name = "v1alpha1"), +/// version(name = "v1beta1"), +/// version(name = "v1"), +/// k8s(group = "example.com") +/// )] +/// #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] +/// pub struct FooSpec { +/// #[versioned( +/// added(since = "v1beta1"), +/// changed( +/// since = "v1", +/// from_name = "prev_bar", +/// from_type = "u16", +/// downgrade_with = usize_to_u16 +/// ) +/// )] +/// bar: usize, +/// baz: bool, +/// } +/// +/// fn usize_to_u16(input: usize) -> u16 { +/// input.try_into().unwrap() +/// } +/// # fn main() {} +/// ``` +/// +/// ## 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 +/// however be noted, that specifying Kubernetes specific arguments is done on the +/// container level instead of on the module level, which is detailed in the +/// following example: +/// +/// ``` +/// # use stackable_versioned_macros::versioned; +/// # use kube::CustomResource; +/// # use schemars::JsonSchema; +/// # use serde::{Deserialize, Serialize}; +/// #[versioned( +/// version(name = "v1alpha1"), +/// version(name = "v1") +/// )] +/// mod versioned { +/// #[versioned(k8s(group = "foo.example.org"))] +/// #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] +/// struct FooSpec { +/// bar: usize, +/// } +/// +/// #[versioned(k8s(group = "bar.example.org"))] +/// #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)] +/// struct BarSpec { +/// baz: String, +/// } +/// } +/// # fn main() {} +/// ``` +/// +///
+/// Expand Generated Code +/// +/// ```ignore +/// mod v1alpha1 { +/// use super::*; +/// #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] +/// #[kube( +/// group = "foo.example.org", +/// version = "v1alpha1", +/// kind = "Foo" +/// )] +/// pub struct FooSpec { +/// pub bar: usize, +/// } +/// +/// #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] +/// #[kube( +/// group = "bar.example.org", +/// version = "v1alpha1", +/// kind = "Bar" +/// )] +/// pub struct BarSpec { +/// pub bar: usize, +/// } +/// } +/// +/// mod v1 { +/// use super::*; +/// #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] +/// #[kube( +/// group = "foo.example.org", +/// version = "v1", +/// kind = "Foo" +/// )] +/// pub struct FooSpec { +/// pub bar: usize, +/// } +/// +/// #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, CustomResource)] +/// #[kube( +/// group = "bar.example.org", +/// version = "v1", +/// kind = "Bar" +/// )] +/// pub struct BarSpec { +/// pub bar: usize, +/// } +/// } +/// ``` +///
+/// +/// 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 #[proc_macro_attribute] pub fn versioned(attrs: TokenStream, input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as Item); @@ -1019,39 +1011,9 @@ fn versioned_impl(attrs: proc_macro2::TokenStream, input: Item) -> proc_macro2:: module.generate_tokens() } - Item::Enum(item_enum) => { - let container_attributes: StandaloneContainerAttributes = - match parse_outer_attributes(attrs) { - Ok(ca) => ca, - Err(err) => return err.write_errors(), - }; - - let standalone_enum = - match StandaloneContainer::new_enum(item_enum, container_attributes) { - Ok(standalone_enum) => standalone_enum, - Err(err) => return err.write_errors(), - }; - - standalone_enum.generate_tokens() - } - Item::Struct(item_struct) => { - let container_attributes: StandaloneContainerAttributes = - match parse_outer_attributes(attrs) { - Ok(ca) => ca, - Err(err) => return err.write_errors(), - }; - - let standalone_struct = - match StandaloneContainer::new_struct(item_struct, container_attributes) { - Ok(standalone_struct) => standalone_struct, - Err(err) => return err.write_errors(), - }; - - standalone_struct.generate_tokens() - } _ => Error::new( input.span(), - "attribute macro `versioned` can be only be applied to modules, structs and enums", + "attribute macro `versioned` can be only be applied to modules", ) .into_compile_error(), } @@ -1071,6 +1033,7 @@ mod snapshot_tests { use super::*; + // TODO (@Techassi): Combine tests, there are no default/k8s-specific tests anymore #[test] fn default() { let _settings_guard = test_utils::set_snapshot_path().bind_to_scope(); @@ -1083,7 +1046,6 @@ mod snapshot_tests { }); } - #[cfg(feature = "k8s")] #[test] fn k8s() { let _settings_guard = test_utils::set_snapshot_path().bind_to_scope(); diff --git a/crates/stackable-versioned-macros/tests/trybuild.rs b/crates/stackable-versioned-macros/tests/trybuild.rs index ff26900a7..a8e6388ff 100644 --- a/crates/stackable-versioned-macros/tests/trybuild.rs +++ b/crates/stackable-versioned-macros/tests/trybuild.rs @@ -21,9 +21,9 @@ mod inputs { // mod attribute_enum; // mod attribute_struct; // mod basic_struct; - // mod downgrade_with; // mod deprecate_enum; // mod deprecate_struct; + // mod downgrade_with; // mod enum_data_simple; // mod generics_defaults; // mod generics_module; @@ -46,7 +46,6 @@ mod inputs { } } - #[cfg(feature = "k8s")] mod k8s { mod pass { // mod basic; @@ -72,7 +71,6 @@ fn default() { t.pass("tests/inputs/default/pass/*.rs"); } -#[cfg(feature = "k8s")] #[test] fn k8s() { let t = trybuild::TestCases::new(); diff --git a/crates/stackable-versioned/Cargo.toml b/crates/stackable-versioned/Cargo.toml index 1db42accd..31638486c 100644 --- a/crates/stackable-versioned/Cargo.toml +++ b/crates/stackable-versioned/Cargo.toml @@ -10,27 +10,15 @@ repository.workspace = true [package.metadata."docs.rs"] all-features = true -[features] -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:serde", - "dep:snafu", -] - [dependencies] -k8s-version = { path = "../k8s-version", features = ["serde"], optional = true } stackable-versioned-macros = { path = "../stackable-versioned-macros" } +k8s-version = { path = "../k8s-version", features = ["serde"] } -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 } +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_yaml.workspace = true +snafu.workspace = true [dev-dependencies] insta.workspace = true diff --git a/crates/stackable-versioned/src/k8s.rs b/crates/stackable-versioned/src/k8s.rs deleted file mode 100644 index f55182fa8..000000000 --- a/crates/stackable-versioned/src/k8s.rs +++ /dev/null @@ -1,113 +0,0 @@ -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. - pub downgrades: HashMap>, - - /// List of values needed when upgrading to a particular version. - 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. - pub name: String, - - /// The value to be used when upgrading or downgrading the custom resource. - #[schemars(schema_with = "raw_object_schema")] - pub value: serde_yaml::Value, -} - -// TODO (@Techassi): Think about where this should live. Basically this already exists in -// stackable-operator, but we cannot use it without depending on it which I would like to -// avoid. -fn raw_object_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema { - Schema::Object(SchemaObject { - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))), - extensions: [( - "x-kubernetes-preserve-unknown-fields".to_owned(), - serde_json::Value::Bool(true), - )] - .into(), - ..Default::default() - }) -} - -/// This error indicates that parsing an object from a conversion review failed. -#[derive(Debug, Snafu)] -pub enum ParseObjectError { - #[snafu(display("the field {field:?} is missing"))] - FieldMissing { field: String }, - - #[snafu(display("the field {field:?} must be a string"))] - FieldNotStr { field: String }, - - #[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 }, - - #[snafu(display("unexpected object kind {kind:?}, expected {expected:?}"))] - UnexpectedKind { kind: String, expected: String }, -} - -/// 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 }, - - #[snafu(display("failed to parse desired API version"))] - ParseDesiredApiVersion { - source: UnknownDesiredApiVersionError, - }, -} - -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, - - // This is likely the clients fault, as it is requesting a unsupported version - ConvertObjectError::ParseDesiredApiVersion { - source: UnknownDesiredApiVersionError { .. }, - } => 400, - } - } -} - -#[derive(Debug, Snafu)] -#[snafu(display("unknown API version {api_version:?}"))] -pub struct UnknownDesiredApiVersionError { - pub api_version: String, -} diff --git a/crates/stackable-versioned/src/lib.rs b/crates/stackable-versioned/src/lib.rs index 5538eae65..5a768b19b 100644 --- a/crates/stackable-versioned/src/lib.rs +++ b/crates/stackable-versioned/src/lib.rs @@ -9,11 +9,147 @@ //! //! See [`versioned`] for an in-depth usage guide and a list of supported arguments. -// Re-exports +use std::collections::HashMap; + +use k8s_version::Version; +use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec}; +use snafu::{ErrorCompat, Snafu}; +// Re-export pub use stackable_versioned_macros::versioned; -// Behind k8s feature -#[cfg(feature = "k8s")] -mod k8s; -#[cfg(feature = "k8s")] -pub use k8s::*; +/// A value-to-value conversion that consumes the input value while tracking changes via a +/// Kubernetes status. +/// +/// This allows nested sub structs to bubble up their tracked changes. +pub trait TrackingFrom +where + Self: Sized, + S: TrackingStatus + Default, +{ + /// Describes the parent field of `Self`. None indicates that `Self` is at the root. + const PARENT: Option<&str>; + + /// Convert `T` into `Self`. + fn tracking_from(value: T, status: &mut S) -> Self; +} + +/// A value-to-value conversion that consumes the input value while tracking changes via a +/// Kubernetes status. The opposite of [`TrackingFrom`]. +/// +/// One should avoid implementing [`TrackingInto`] as it is automatically implemented via a +/// blanket implementation. +pub trait TrackingInto +where + Self: Sized, + S: TrackingStatus + Default, +{ + /// Convert `Self` into `T`. + fn tracking_into(self, status: &mut S) -> T; +} + +impl TrackingInto for T +where + S: TrackingStatus + Default, + U: TrackingFrom, +{ + fn tracking_into(self, status: &mut S) -> U { + U::tracking_from(self, status) + } +} + +/// Used to access [`ChangedValues`] from any status. +pub trait TrackingStatus { + fn changes(&mut self) -> &mut ChangedValues; +} + +// 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, Default, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +pub struct ChangedValues { + /// List of values needed when downgrading to a particular version. + pub downgrades: HashMap>, + + /// List of values needed when upgrading to a particular version. + pub upgrades: HashMap>, + // TODO (@Techassi): Add a version indicator here if we ever decide to change the tracking + // mechanism. +} + +/// Contains a changed value for a single field of the CRD. +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct ChangedValue { + /// The name of the field of the custom resource this value is for. + pub field_name: String, + + /// The value to be used when upgrading or downgrading the custom resource. + #[schemars(schema_with = "raw_object_schema")] + pub value: serde_yaml::Value, +} + +// TODO (@Techassi): Think about where this should live. Basically this already exists in +// stackable-operator, but we cannot use it without depending on it which I would like to +// avoid. +fn raw_object_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema { + Schema::Object(SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))), + extensions: [( + "x-kubernetes-preserve-unknown-fields".to_owned(), + serde_json::Value::Bool(true), + )] + .into(), + ..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/xtask/src/crd/dummy.rs b/crates/xtask/src/crd/dummy.rs index 6c5fd7871..3f0652e80 100644 --- a/crates/xtask/src/crd/dummy.rs +++ b/crates/xtask/src/crd/dummy.rs @@ -4,25 +4,27 @@ use stackable_operator::{ config::fragment::Fragment, kube::CustomResource, role_utils::Role, - schemars::JsonSchema, + schemars::{self, JsonSchema}, status::condition::ClusterCondition, versioned::versioned, }; -#[versioned(version(name = "v1alpha1"))] +#[versioned( + version(name = "v1alpha1"), + crates( + kube_core = "stackable_operator::kube::core", + kube_client = "stackable_operator::kube::client", + k8s_openapi = "stackable_operator::k8s_openapi", + schemars = "stackable_operator::schemars", + versioned = "stackable_operator::versioned" + ) +)] pub mod versioned { - #[versioned(k8s( + #[versioned(crd( group = "dummy.stackable.tech", kind = "DummyCluster", status = "v1alpha1::DummyClusterStatus", namespaced, - crates( - kube_core = "stackable_operator::kube::core", - kube_client = "stackable_operator::kube::client", - k8s_openapi = "stackable_operator::k8s_openapi", - schemars = "stackable_operator::schemars", - versioned = "stackable_operator::versioned" - ) ))] #[derive(Clone, CustomResource, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] #[schemars(crate = "stackable_operator::schemars")]