diff --git a/Cargo.lock b/Cargo.lock index 2d3130e8e..3a2fe630d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,29 +158,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" -[[package]] -name = "aws-lc-rs" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" -dependencies = [ - "bindgen", - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "axum" version = "0.8.4" @@ -285,29 +262,6 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.101", - "which", -] - [[package]] name = "bit-set" version = "0.5.3" @@ -372,20 +326,9 @@ version = "1.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" dependencies = [ - "jobserver", - "libc", "shlex", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -402,17 +345,6 @@ dependencies = [ "serde", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "4.5.38" @@ -453,15 +385,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" -[[package]] -name = "cmake" -version = "0.1.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" -dependencies = [ - "cc", -] - [[package]] name = "colorchoice" version = "1.0.3" @@ -747,12 +670,6 @@ dependencies = [ "snafu 0.6.10", ] -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "dyn-clone" version = "1.0.19" @@ -956,12 +873,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futures" version = "0.3.31" @@ -1531,15 +1442,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -1566,16 +1468,6 @@ dependencies = [ "regex", ] -[[package]] -name = "jobserver" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" -dependencies = [ - "getrandom 0.3.3", - "libc", -] - [[package]] name = "js-sys" version = "0.3.77" @@ -1653,8 +1545,7 @@ dependencies = [ [[package]] name = "kube" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b49c39074089233c2bb7b1791d1b6c06c84dbab26757491fad9d233db0d432f" +source = "git+https://github.com/kube-rs/kube.git?rev=d1ad7ce1aad0d8c527ede404047778885c552034#d1ad7ce1aad0d8c527ede404047778885c552034" dependencies = [ "k8s-openapi", "kube-client", @@ -1666,8 +1557,7 @@ dependencies = [ [[package]] name = "kube-client" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e199797b1b08865041c9c698f0d11a91de0a8143e808b71e250cd4a1d7ce2b9f" +source = "git+https://github.com/kube-rs/kube.git?rev=d1ad7ce1aad0d8c527ede404047778885c552034#d1ad7ce1aad0d8c527ede404047778885c552034" dependencies = [ "base64 0.22.1", "bytes", @@ -1703,8 +1593,7 @@ dependencies = [ [[package]] name = "kube-core" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bdefbba89dea2d99ea822a1d7cd6945535efbfb10b790056ee9284bf9e698e7" +source = "git+https://github.com/kube-rs/kube.git?rev=d1ad7ce1aad0d8c527ede404047778885c552034#d1ad7ce1aad0d8c527ede404047778885c552034" dependencies = [ "chrono", "derive_more", @@ -1722,8 +1611,7 @@ dependencies = [ [[package]] name = "kube-derive" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e609a3633689a50869352a3c16e01d863b6137863c80eeb038383d5ab9f83bf" +source = "git+https://github.com/kube-rs/kube.git?rev=d1ad7ce1aad0d8c527ede404047778885c552034#d1ad7ce1aad0d8c527ede404047778885c552034" dependencies = [ "darling", "proc-macro2", @@ -1736,8 +1624,7 @@ dependencies = [ [[package]] name = "kube-runtime" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d4bd8a4554786f8f9a87bfa977fb7dbaa1d7f102a30477338b044b65de29d8e" +source = "git+https://github.com/kube-rs/kube.git?rev=d1ad7ce1aad0d8c527ede404047778885c552034#d1ad7ce1aad0d8c527ede404047778885c552034" dependencies = [ "ahash", "async-broadcast", @@ -1769,40 +1656,18 @@ dependencies = [ "spin", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" -[[package]] -name = "libloading" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" -dependencies = [ - "cfg-if", - "windows-targets 0.53.0", -] - [[package]] name = "libm" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -1858,12 +1723,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.8" @@ -1884,16 +1743,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2343,7 +2192,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools", "proc-macro2", "quote", "syn 2.0.101", @@ -2611,12 +2460,6 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc_version" version = "0.4.1" @@ -2626,19 +2469,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - [[package]] name = "rustix" version = "1.0.7" @@ -2648,7 +2478,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.9.4", + "linux-raw-sys", "windows-sys 0.59.0", ] @@ -2658,7 +2488,6 @@ version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ - "aws-lc-rs", "log", "once_cell", "ring", @@ -2717,7 +2546,6 @@ version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -3210,7 +3038,15 @@ dependencies = [ name = "stackable-versioned" version = "0.7.1" dependencies = [ + "insta", + "k8s-openapi", + "kube", + "schemars", + "serde", + "serde_json", + "snafu 0.8.5", "stackable-versioned-macros", + "tracing", ] [[package]] @@ -3220,7 +3056,7 @@ dependencies = [ "convert_case", "darling", "insta", - "itertools 0.14.0", + "itertools", "k8s-openapi", "k8s-version", "kube", @@ -3236,6 +3072,7 @@ dependencies = [ "snafu 0.8.5", "stackable-versioned", "syn 2.0.101", + "tracing", "trybuild", ] @@ -3247,10 +3084,7 @@ dependencies = [ "futures-util", "hyper", "hyper-util", - "k8s-openapi", - "kube", "opentelemetry", - "serde_json", "snafu 0.8.5", "stackable-certs", "stackable-operator", @@ -3261,6 +3095,24 @@ dependencies = [ "tower-http", "tracing", "tracing-opentelemetry", + "x509-cert", +] + +[[package]] +name = "stackable-webhook-example" +version = "0.0.1" +dependencies = [ + "k8s-openapi", + "kube", + "schemars", + "serde", + "serde_json", + "snafu 0.8.5", + "stackable-operator", + "stackable-webhook", + "tokio", + "tracing", + "tracing-subscriber", ] [[package]] @@ -3354,7 +3206,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.7", + "rustix", "windows-sys 0.59.0", ] @@ -4006,18 +3858,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 4c1163a73..2e63b12e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ repository = "https://github.com/stackabletech/operator-rs" [workspace.dependencies] product-config = { git = "https://github.com/stackabletech/product-config.git", tag = "0.7.0" } -axum = "0.8.1" +axum = { version = "0.8.1", features = ["http2"] } chrono = { version = "0.4.38", default-features = false } clap = { version = "4.5.17", features = ["derive", "cargo", "env"] } const_format = "0.2.33" @@ -33,6 +33,7 @@ itertools = "0.14.0" json-patch = "4.0.0" k8s-openapi = { version = "0.25.0", default-features = false, features = ["schemars", "v1_33"] } # We use rustls instead of openssl for easier portability, e.g. so that we can build stackablectl without the need to vendor (build from source) openssl +# Use ring instead of aws-lc-rs, as this currently fails to build in "make run-dev" kube = { version = "1.0.0", default-features = false, features = ["client", "jsonpatch", "runtime", "derive", "rustls-tls", "ring"] } opentelemetry = "0.29.1" opentelemetry_sdk = { version = "0.29.0", features = ["rt-tokio"] } @@ -64,7 +65,8 @@ syn = "2.0.77" tempfile = "3.12.0" time = { version = "0.3.36" } tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread", "fs"] } -tokio-rustls = "0.26.0" +# Use ring instead of aws-lc-rs, as this currently fails to build in "make run-dev" +tokio-rustls = { version = "0.26.0", default-features = false, features = ["ring", "logging", "tls12"] } tokio-test = "0.4.4" tower = { version = "0.5.1", features = ["util"] } tower-http = { version = "0.6.1", features = ["trace"] } @@ -87,3 +89,7 @@ rsa.opt-level = 3 [profile.dev.package] insta.opt-level = 3 similar.opt-level = 3 + +[patch.crates-io] +# https://github.com/kube-rs/kube/pull/1759 will be in 1.1.0 +kube = { git = 'https://github.com/kube-rs/kube.git', rev = "d1ad7ce1aad0d8c527ede404047778885c552034" } diff --git a/crates/stackable-operator/Cargo.toml b/crates/stackable-operator/Cargo.toml index 906a184e3..2de143f6f 100644 --- a/crates/stackable-operator/Cargo.toml +++ b/crates/stackable-operator/Cargo.toml @@ -16,7 +16,7 @@ versioned = [] [dependencies] stackable-telemetry = { path = "../stackable-telemetry", features = ["clap"] } -stackable-versioned = { path = "../stackable-versioned", features = ["k8s"] } +stackable-versioned = { path = "../stackable-versioned", features = ["k8s", "flux-converter"] } stackable-operator-derive = { path = "../stackable-operator-derive" } stackable-shared = { path = "../stackable-shared" } diff --git a/crates/stackable-versioned-macros/Cargo.toml b/crates/stackable-versioned-macros/Cargo.toml index 123679118..1dd847ec7 100644 --- a/crates/stackable-versioned-macros/Cargo.toml +++ b/crates/stackable-versioned-macros/Cargo.toml @@ -25,8 +25,9 @@ normal = ["k8s-openapi", "kube"] proc-macro = true [features] -full = ["k8s"] +full = ["k8s", "flux-converter"] k8s = ["dep:kube", "dep:k8s-openapi"] +flux-converter = ["k8s"] [dependencies] k8s-version = { path = "../k8s-version", features = ["darling"] } @@ -54,4 +55,5 @@ serde.workspace = true serde_json.workspace = true serde_yaml.workspace = true snafu.workspace = true +tracing.workspace = true trybuild.workspace = true diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@basic.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@basic.rs.snap index 83bab4878..8fd1a1468 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@basic.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@basic.rs.snap @@ -111,6 +111,41 @@ impl ::std::fmt::Display for Foo { } } } +/// Parses the version, such as `v1alpha1` +#[automatically_derived] +impl ::std::str::FromStr for Foo { + type Err = stackable_versioned::ParseResourceVersionError; + fn from_str(version: &str) -> Result { + match version { + "v1alpha1" => Ok(Self::V1Alpha1), + "v1beta1" => Ok(Self::V1Beta1), + "v1" => Ok(Self::V1), + _ => { + Err(stackable_versioned::ParseResourceVersionError::UnknownResourceVersion { + version: version.to_string(), + }) + } + } + } +} +/// Parses the entire `apiVersion`, such as `zookeeper.stackable.tech/v1alpha1`. +#[automatically_derived] +impl Foo { + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "stackable.tech/v1alpha1" => Ok(Self::V1Alpha1), + "stackable.tech/v1beta1" => Ok(Self::V1Beta1), + "stackable.tech/v1" => Ok(Self::V1), + _ => { + Err(stackable_versioned::ParseResourceVersionError::UnknownApiVersion { + api_version: api_version.to_string(), + }) + } + } + } +} #[automatically_derived] impl Foo { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. @@ -130,3 +165,392 @@ impl Foo { ) } } +#[automatically_derived] +impl Foo { + #[tracing::instrument( + skip_all, + fields( + conversion.kind = review.types.kind, + conversion.api_version = review.types.api_version, + ) + )] + pub fn convert( + review: ::kube::core::conversion::ConversionReview, + ) -> ::kube::core::conversion::ConversionReview { + use ::kube::core::conversion::{ConversionRequest, ConversionResponse}; + use ::kube::core::response::StatusSummary; + use stackable_versioned::ConversionError; + let request = match ConversionRequest::from_review(review) { + Ok(request) => request, + Err(err) => { + tracing::warn!( + ? err, + "Invalid ConversionReview send by Kubernetes apiserver. It probably did not include a request" + ); + return ConversionResponse::invalid(kube::client::Status { + status: Some(StatusSummary::Failure), + code: 400, + message: format!( + "The ConversionReview send did not include any request: {err}" + ), + reason: "ConversionReview request missing".to_string(), + details: None, + }) + .into_review(); + } + }; + let converted = Self::try_convert(&request); + let conversion_response = ConversionResponse::for_request(request); + match converted { + Ok(converted) => { + tracing::debug!( + "Successfully converted {num} objects of type {type}", num = + converted.len(), type = stringify!(Foo), + ); + conversion_response.success(converted).into_review() + } + Err(err) => { + let error_message = err.as_human_readable_error_message(); + conversion_response + .failure(kube::client::Status { + status: Some(StatusSummary::Failure), + code: err.http_return_code(), + message: error_message.clone(), + reason: error_message, + details: None, + }) + .into_review() + } + } + } + #[tracing::instrument(skip_all, err)] + fn try_convert( + request: &::kube::core::conversion::ConversionRequest, + ) -> Result, stackable_versioned::ConversionError> { + use stackable_versioned::ConversionError; + let desired_object_version = Self::from_api_version(&request.desired_api_version) + .map_err(|err| ConversionError::ParseDesiredResourceVersion { + source: err, + version: request.desired_api_version.to_string(), + })?; + let mut converted: Vec = Vec::with_capacity( + request.objects.len(), + ); + for object in &request.objects { + let object_spec = object + .get("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })?; + let object_kind = object + .get("kind") + .ok_or_else(|| ConversionError::ObjectHasNoKind { + })?; + let object_kind = object_kind + .as_str() + .ok_or_else(|| ConversionError::ObjectKindNotString { + kind: object_kind.clone(), + })?; + let object_version = object + .get("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })?; + let object_version = object_version + .as_str() + .ok_or_else(|| ConversionError::ObjectApiVersionNotString { + api_version: object_version.clone(), + })?; + if object_kind != stringify!(Foo) { + return Err(ConversionError::WrongObjectKind { + expected_kind: stringify!(Foo).to_string(), + send_kind: object_kind.to_string(), + }); + } + let current_object_version = Self::from_api_version(object_version) + .map_err(|err| ConversionError::ParseCurrentResourceVersion { + source: err, + version: object_version.to_string(), + })?; + match (¤t_object_version, &desired_object_version) { + (Self::V1Alpha1, Self::V1Alpha1) => { + let resource_spec: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1alpha1"), + conversion.steps = 0usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Alpha1, Self::V1Beta1) => { + let resource_spec: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1beta1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1beta1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Alpha1, Self::V1) => { + let resource_spec: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1beta1::FooSpec = resource_spec.into(); + let resource_spec: v1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1"), conversion + .steps = 2usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Beta1, Self::V1Alpha1) => { + let resource_spec: v1beta1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1alpha1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1beta1), to = stringify!("v1alpha1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Beta1, Self::V1Beta1) => { + let resource_spec: v1beta1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + tracing::trace!( + from = stringify!(v1beta1), to = stringify!("v1beta1"), + conversion.steps = 0usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Beta1, Self::V1) => { + let resource_spec: v1beta1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1beta1), to = stringify!("v1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1Alpha1) => { + let resource_spec: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1beta1::FooSpec = resource_spec.into(); + let resource_spec: v1alpha1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1alpha1"), conversion + .steps = 2usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1Beta1) => { + let resource_spec: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1beta1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1beta1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1) => { + let resource_spec: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + tracing::trace!( + from = stringify!(v1), to = stringify!("v1"), conversion.steps = + 0usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + } + } + Ok(converted) + } +} diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@crate_overrides.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@crate_overrides.rs.snap index 2999586ad..89bf7798c 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@crate_overrides.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@crate_overrides.rs.snap @@ -114,6 +114,41 @@ impl ::std::fmt::Display for Foo { } } } +/// Parses the version, such as `v1alpha1` +#[automatically_derived] +impl ::std::str::FromStr for Foo { + type Err = stackable_versioned::ParseResourceVersionError; + fn from_str(version: &str) -> Result { + match version { + "v1alpha1" => Ok(Self::V1Alpha1), + "v1beta1" => Ok(Self::V1Beta1), + "v1" => Ok(Self::V1), + _ => { + Err(stackable_versioned::ParseResourceVersionError::UnknownResourceVersion { + version: version.to_string(), + }) + } + } + } +} +/// Parses the entire `apiVersion`, such as `zookeeper.stackable.tech/v1alpha1`. +#[automatically_derived] +impl Foo { + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "foo.example.org/v1alpha1" => Ok(Self::V1Alpha1), + "foo.example.org/v1beta1" => Ok(Self::V1Beta1), + "foo.example.org/v1" => Ok(Self::V1), + _ => { + Err(stackable_versioned::ParseResourceVersionError::UnknownApiVersion { + api_version: api_version.to_string(), + }) + } + } + } +} #[automatically_derived] impl Foo { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. @@ -133,3 +168,392 @@ impl Foo { ) } } +#[automatically_derived] +impl Foo { + #[tracing::instrument( + skip_all, + fields( + conversion.kind = review.types.kind, + conversion.api_version = review.types.api_version, + ) + )] + pub fn convert( + review: ::kube::core::conversion::ConversionReview, + ) -> ::kube::core::conversion::ConversionReview { + use ::kube::core::conversion::{ConversionRequest, ConversionResponse}; + use ::kube::core::response::StatusSummary; + use stackable_versioned::ConversionError; + let request = match ConversionRequest::from_review(review) { + Ok(request) => request, + Err(err) => { + tracing::warn!( + ? err, + "Invalid ConversionReview send by Kubernetes apiserver. It probably did not include a request" + ); + return ConversionResponse::invalid(kube::client::Status { + status: Some(StatusSummary::Failure), + code: 400, + message: format!( + "The ConversionReview send did not include any request: {err}" + ), + reason: "ConversionReview request missing".to_string(), + details: None, + }) + .into_review(); + } + }; + let converted = Self::try_convert(&request); + let conversion_response = ConversionResponse::for_request(request); + match converted { + Ok(converted) => { + tracing::debug!( + "Successfully converted {num} objects of type {type}", num = + converted.len(), type = stringify!(Foo), + ); + conversion_response.success(converted).into_review() + } + Err(err) => { + let error_message = err.as_human_readable_error_message(); + conversion_response + .failure(kube::client::Status { + status: Some(StatusSummary::Failure), + code: err.http_return_code(), + message: error_message.clone(), + reason: error_message, + details: None, + }) + .into_review() + } + } + } + #[tracing::instrument(skip_all, err)] + fn try_convert( + request: &::kube::core::conversion::ConversionRequest, + ) -> Result, stackable_versioned::ConversionError> { + use stackable_versioned::ConversionError; + let desired_object_version = Self::from_api_version(&request.desired_api_version) + .map_err(|err| ConversionError::ParseDesiredResourceVersion { + source: err, + version: request.desired_api_version.to_string(), + })?; + let mut converted: Vec = Vec::with_capacity( + request.objects.len(), + ); + for object in &request.objects { + let object_spec = object + .get("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })?; + let object_kind = object + .get("kind") + .ok_or_else(|| ConversionError::ObjectHasNoKind { + })?; + let object_kind = object_kind + .as_str() + .ok_or_else(|| ConversionError::ObjectKindNotString { + kind: object_kind.clone(), + })?; + let object_version = object + .get("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })?; + let object_version = object_version + .as_str() + .ok_or_else(|| ConversionError::ObjectApiVersionNotString { + api_version: object_version.clone(), + })?; + if object_kind != stringify!(Foo) { + return Err(ConversionError::WrongObjectKind { + expected_kind: stringify!(Foo).to_string(), + send_kind: object_kind.to_string(), + }); + } + let current_object_version = Self::from_api_version(object_version) + .map_err(|err| ConversionError::ParseCurrentResourceVersion { + source: err, + version: object_version.to_string(), + })?; + match (¤t_object_version, &desired_object_version) { + (Self::V1Alpha1, Self::V1Alpha1) => { + let resource_spec: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1alpha1"), + conversion.steps = 0usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Alpha1, Self::V1Beta1) => { + let resource_spec: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1beta1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1beta1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Alpha1, Self::V1) => { + let resource_spec: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1beta1::FooSpec = resource_spec.into(); + let resource_spec: v1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1"), conversion + .steps = 2usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Beta1, Self::V1Alpha1) => { + let resource_spec: v1beta1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1alpha1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1beta1), to = stringify!("v1alpha1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Beta1, Self::V1Beta1) => { + let resource_spec: v1beta1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + tracing::trace!( + from = stringify!(v1beta1), to = stringify!("v1beta1"), + conversion.steps = 0usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Beta1, Self::V1) => { + let resource_spec: v1beta1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1beta1), to = stringify!("v1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1Alpha1) => { + let resource_spec: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1beta1::FooSpec = resource_spec.into(); + let resource_spec: v1alpha1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1alpha1"), conversion + .steps = 2usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1Beta1) => { + let resource_spec: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1beta1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1beta1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1) => { + let resource_spec: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + tracing::trace!( + from = stringify!(v1), to = stringify!("v1"), conversion.steps = + 0usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + } + } + Ok(converted) + } +} diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@module.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@module.rs.snap index d01dbc544..0e14d19d6 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@module.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@module.rs.snap @@ -228,6 +228,41 @@ impl ::std::fmt::Display for Foo { } } } +/// Parses the version, such as `v1alpha1` +#[automatically_derived] +impl ::std::str::FromStr for Foo { + type Err = stackable_versioned::ParseResourceVersionError; + fn from_str(version: &str) -> Result { + match version { + "v1alpha1" => Ok(Self::V1Alpha1), + "v1" => Ok(Self::V1), + "v2alpha1" => Ok(Self::V2Alpha1), + _ => { + Err(stackable_versioned::ParseResourceVersionError::UnknownResourceVersion { + version: version.to_string(), + }) + } + } + } +} +/// Parses the entire `apiVersion`, such as `zookeeper.stackable.tech/v1alpha1`. +#[automatically_derived] +impl Foo { + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "foo.example.org/v1alpha1" => Ok(Self::V1Alpha1), + "foo.example.org/v1" => Ok(Self::V1), + "foo.example.org/v2alpha1" => Ok(Self::V2Alpha1), + _ => { + Err(stackable_versioned::ParseResourceVersionError::UnknownApiVersion { + api_version: api_version.to_string(), + }) + } + } + } +} #[automatically_derived] impl Foo { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. @@ -248,6 +283,395 @@ impl Foo { } } #[automatically_derived] +impl Foo { + #[tracing::instrument( + skip_all, + fields( + conversion.kind = review.types.kind, + conversion.api_version = review.types.api_version, + ) + )] + pub fn convert( + review: ::kube::core::conversion::ConversionReview, + ) -> ::kube::core::conversion::ConversionReview { + use ::kube::core::conversion::{ConversionRequest, ConversionResponse}; + use ::kube::core::response::StatusSummary; + use stackable_versioned::ConversionError; + let request = match ConversionRequest::from_review(review) { + Ok(request) => request, + Err(err) => { + tracing::warn!( + ? err, + "Invalid ConversionReview send by Kubernetes apiserver. It probably did not include a request" + ); + return ConversionResponse::invalid(kube::client::Status { + status: Some(StatusSummary::Failure), + code: 400, + message: format!( + "The ConversionReview send did not include any request: {err}" + ), + reason: "ConversionReview request missing".to_string(), + details: None, + }) + .into_review(); + } + }; + let converted = Self::try_convert(&request); + let conversion_response = ConversionResponse::for_request(request); + match converted { + Ok(converted) => { + tracing::debug!( + "Successfully converted {num} objects of type {type}", num = + converted.len(), type = stringify!(Foo), + ); + conversion_response.success(converted).into_review() + } + Err(err) => { + let error_message = err.as_human_readable_error_message(); + conversion_response + .failure(kube::client::Status { + status: Some(StatusSummary::Failure), + code: err.http_return_code(), + message: error_message.clone(), + reason: error_message, + details: None, + }) + .into_review() + } + } + } + #[tracing::instrument(skip_all, err)] + fn try_convert( + request: &::kube::core::conversion::ConversionRequest, + ) -> Result, stackable_versioned::ConversionError> { + use stackable_versioned::ConversionError; + let desired_object_version = Self::from_api_version(&request.desired_api_version) + .map_err(|err| ConversionError::ParseDesiredResourceVersion { + source: err, + version: request.desired_api_version.to_string(), + })?; + let mut converted: Vec = Vec::with_capacity( + request.objects.len(), + ); + for object in &request.objects { + let object_spec = object + .get("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })?; + let object_kind = object + .get("kind") + .ok_or_else(|| ConversionError::ObjectHasNoKind { + })?; + let object_kind = object_kind + .as_str() + .ok_or_else(|| ConversionError::ObjectKindNotString { + kind: object_kind.clone(), + })?; + let object_version = object + .get("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })?; + let object_version = object_version + .as_str() + .ok_or_else(|| ConversionError::ObjectApiVersionNotString { + api_version: object_version.clone(), + })?; + if object_kind != stringify!(Foo) { + return Err(ConversionError::WrongObjectKind { + expected_kind: stringify!(Foo).to_string(), + send_kind: object_kind.to_string(), + }); + } + let current_object_version = Self::from_api_version(object_version) + .map_err(|err| ConversionError::ParseCurrentResourceVersion { + source: err, + version: object_version.to_string(), + })?; + match (¤t_object_version, &desired_object_version) { + (Self::V1Alpha1, Self::V1Alpha1) => { + let resource_spec: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1alpha1"), + conversion.steps = 0usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Alpha1, Self::V1) => { + let resource_spec: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Alpha1, Self::V2Alpha1) => { + let resource_spec: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1::FooSpec = resource_spec.into(); + let resource_spec: v2alpha1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v2alpha1"), + conversion.steps = 2usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1Alpha1) => { + let resource_spec: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1alpha1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1alpha1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1) => { + let resource_spec: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + tracing::trace!( + from = stringify!(v1), to = stringify!("v1"), conversion.steps = + 0usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V2Alpha1) => { + let resource_spec: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v2alpha1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v2alpha1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V2Alpha1, Self::V1Alpha1) => { + let resource_spec: v2alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1::FooSpec = resource_spec.into(); + let resource_spec: v1alpha1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v1alpha1"), + conversion.steps = 2usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V2Alpha1, Self::V1) => { + let resource_spec: v2alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V2Alpha1, Self::V2Alpha1) => { + let resource_spec: v2alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v2alpha1"), + conversion.steps = 0usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + } + } + Ok(converted) + } +} +#[automatically_derived] pub(crate) enum Bar { V1Alpha1, V1, @@ -266,6 +690,41 @@ impl ::std::fmt::Display for Bar { } } } +/// Parses the version, such as `v1alpha1` +#[automatically_derived] +impl ::std::str::FromStr for Bar { + type Err = stackable_versioned::ParseResourceVersionError; + fn from_str(version: &str) -> Result { + match version { + "v1alpha1" => Ok(Self::V1Alpha1), + "v1" => Ok(Self::V1), + "v2alpha1" => Ok(Self::V2Alpha1), + _ => { + Err(stackable_versioned::ParseResourceVersionError::UnknownResourceVersion { + version: version.to_string(), + }) + } + } + } +} +/// Parses the entire `apiVersion`, such as `zookeeper.stackable.tech/v1alpha1`. +#[automatically_derived] +impl Bar { + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "bar.example.org/v1alpha1" => Ok(Self::V1Alpha1), + "bar.example.org/v1" => Ok(Self::V1), + "bar.example.org/v2alpha1" => Ok(Self::V2Alpha1), + _ => { + Err(stackable_versioned::ParseResourceVersionError::UnknownApiVersion { + api_version: api_version.to_string(), + }) + } + } + } +} #[automatically_derived] impl Bar { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. @@ -285,3 +744,392 @@ impl Bar { ) } } +#[automatically_derived] +impl Bar { + #[tracing::instrument( + skip_all, + fields( + conversion.kind = review.types.kind, + conversion.api_version = review.types.api_version, + ) + )] + pub fn convert( + review: ::kube::core::conversion::ConversionReview, + ) -> ::kube::core::conversion::ConversionReview { + use ::kube::core::conversion::{ConversionRequest, ConversionResponse}; + use ::kube::core::response::StatusSummary; + use stackable_versioned::ConversionError; + let request = match ConversionRequest::from_review(review) { + Ok(request) => request, + Err(err) => { + tracing::warn!( + ? err, + "Invalid ConversionReview send by Kubernetes apiserver. It probably did not include a request" + ); + return ConversionResponse::invalid(kube::client::Status { + status: Some(StatusSummary::Failure), + code: 400, + message: format!( + "The ConversionReview send did not include any request: {err}" + ), + reason: "ConversionReview request missing".to_string(), + details: None, + }) + .into_review(); + } + }; + let converted = Self::try_convert(&request); + let conversion_response = ConversionResponse::for_request(request); + match converted { + Ok(converted) => { + tracing::debug!( + "Successfully converted {num} objects of type {type}", num = + converted.len(), type = stringify!(Bar), + ); + conversion_response.success(converted).into_review() + } + Err(err) => { + let error_message = err.as_human_readable_error_message(); + conversion_response + .failure(kube::client::Status { + status: Some(StatusSummary::Failure), + code: err.http_return_code(), + message: error_message.clone(), + reason: error_message, + details: None, + }) + .into_review() + } + } + } + #[tracing::instrument(skip_all, err)] + fn try_convert( + request: &::kube::core::conversion::ConversionRequest, + ) -> Result, stackable_versioned::ConversionError> { + use stackable_versioned::ConversionError; + let desired_object_version = Self::from_api_version(&request.desired_api_version) + .map_err(|err| ConversionError::ParseDesiredResourceVersion { + source: err, + version: request.desired_api_version.to_string(), + })?; + let mut converted: Vec = Vec::with_capacity( + request.objects.len(), + ); + for object in &request.objects { + let object_spec = object + .get("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })?; + let object_kind = object + .get("kind") + .ok_or_else(|| ConversionError::ObjectHasNoKind { + })?; + let object_kind = object_kind + .as_str() + .ok_or_else(|| ConversionError::ObjectKindNotString { + kind: object_kind.clone(), + })?; + let object_version = object + .get("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })?; + let object_version = object_version + .as_str() + .ok_or_else(|| ConversionError::ObjectApiVersionNotString { + api_version: object_version.clone(), + })?; + if object_kind != stringify!(Bar) { + return Err(ConversionError::WrongObjectKind { + expected_kind: stringify!(Bar).to_string(), + send_kind: object_kind.to_string(), + }); + } + let current_object_version = Self::from_api_version(object_version) + .map_err(|err| ConversionError::ParseCurrentResourceVersion { + source: err, + version: object_version.to_string(), + })?; + match (¤t_object_version, &desired_object_version) { + (Self::V1Alpha1, Self::V1Alpha1) => { + let resource_spec: v1alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1alpha1"), + conversion.steps = 0usize, + "Successfully converted {type} object", type = stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Alpha1, Self::V1) => { + let resource_spec: v1alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource_spec: v1::BarSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Alpha1, Self::V2Alpha1) => { + let resource_spec: v1alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource_spec: v1::BarSpec = resource_spec.into(); + let resource_spec: v2alpha1::BarSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v2alpha1"), + conversion.steps = 2usize, + "Successfully converted {type} object", type = stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1Alpha1) => { + let resource_spec: v1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource_spec: v1alpha1::BarSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1alpha1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1) => { + let resource_spec: v1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + tracing::trace!( + from = stringify!(v1), to = stringify!("v1"), conversion.steps = + 0usize, "Successfully converted {type} object", type = + stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V2Alpha1) => { + let resource_spec: v1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource_spec: v2alpha1::BarSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v2alpha1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V2Alpha1, Self::V1Alpha1) => { + let resource_spec: v2alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource_spec: v1::BarSpec = resource_spec.into(); + let resource_spec: v1alpha1::BarSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v1alpha1"), + conversion.steps = 2usize, + "Successfully converted {type} object", type = stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V2Alpha1, Self::V1) => { + let resource_spec: v2alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource_spec: v1::BarSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V2Alpha1, Self::V2Alpha1) => { + let resource_spec: v2alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v2alpha1"), + conversion.steps = 0usize, + "Successfully converted {type} object", type = stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + } + } + Ok(converted) + } +} diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@module_preserve.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@module_preserve.rs.snap index 601a8a0a9..a7fb2446a 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@module_preserve.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@module_preserve.rs.snap @@ -217,6 +217,39 @@ pub(crate) mod versioned { } } } + /// Parses the version, such as `v1alpha1` + impl ::std::str::FromStr for Foo { + type Err = stackable_versioned::ParseResourceVersionError; + fn from_str(version: &str) -> Result { + match version { + "v1alpha1" => Ok(Self::V1Alpha1), + "v1" => Ok(Self::V1), + "v2alpha1" => Ok(Self::V2Alpha1), + _ => { + Err(stackable_versioned::ParseResourceVersionError::UnknownResourceVersion { + version: version.to_string(), + }) + } + } + } + } + /// Parses the entire `apiVersion`, such as `zookeeper.stackable.tech/v1alpha1`. + impl Foo { + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "foo.example.org/v1alpha1" => Ok(Self::V1Alpha1), + "foo.example.org/v1" => Ok(Self::V1), + "foo.example.org/v2alpha1" => Ok(Self::V2Alpha1), + _ => { + Err(stackable_versioned::ParseResourceVersionError::UnknownApiVersion { + api_version: api_version.to_string(), + }) + } + } + } + } impl Foo { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( @@ -235,6 +268,405 @@ pub(crate) mod versioned { ) } } + #[automatically_derived] + impl Foo { + #[tracing::instrument( + skip_all, + fields( + conversion.kind = review.types.kind, + conversion.api_version = review.types.api_version, + ) + )] + pub fn convert( + review: ::kube::core::conversion::ConversionReview, + ) -> ::kube::core::conversion::ConversionReview { + use ::kube::core::conversion::{ConversionRequest, ConversionResponse}; + use ::kube::core::response::StatusSummary; + use stackable_versioned::ConversionError; + let request = match ConversionRequest::from_review(review) { + Ok(request) => request, + Err(err) => { + tracing::warn!( + ? err, + "Invalid ConversionReview send by Kubernetes apiserver. It probably did not include a request" + ); + return ConversionResponse::invalid(kube::client::Status { + status: Some(StatusSummary::Failure), + code: 400, + message: format!( + "The ConversionReview send did not include any request: {err}" + ), + reason: "ConversionReview request missing".to_string(), + details: None, + }) + .into_review(); + } + }; + let converted = Self::try_convert(&request); + let conversion_response = ConversionResponse::for_request(request); + match converted { + Ok(converted) => { + tracing::debug!( + "Successfully converted {num} objects of type {type}", num = + converted.len(), type = stringify!(Foo), + ); + conversion_response.success(converted).into_review() + } + Err(err) => { + let error_message = err.as_human_readable_error_message(); + conversion_response + .failure(kube::client::Status { + status: Some(StatusSummary::Failure), + code: err.http_return_code(), + message: error_message.clone(), + reason: error_message, + details: None, + }) + .into_review() + } + } + } + #[tracing::instrument(skip_all, err)] + fn try_convert( + request: &::kube::core::conversion::ConversionRequest, + ) -> Result, stackable_versioned::ConversionError> { + use stackable_versioned::ConversionError; + let desired_object_version = Self::from_api_version( + &request.desired_api_version, + ) + .map_err(|err| ConversionError::ParseDesiredResourceVersion { + source: err, + version: request.desired_api_version.to_string(), + })?; + let mut converted: Vec = Vec::with_capacity( + request.objects.len(), + ); + for object in &request.objects { + let object_spec = object + .get("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })?; + let object_kind = object + .get("kind") + .ok_or_else(|| ConversionError::ObjectHasNoKind { + })?; + let object_kind = object_kind + .as_str() + .ok_or_else(|| ConversionError::ObjectKindNotString { + kind: object_kind.clone(), + })?; + let object_version = object + .get("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })?; + let object_version = object_version + .as_str() + .ok_or_else(|| ConversionError::ObjectApiVersionNotString { + api_version: object_version.clone(), + })?; + if object_kind != stringify!(Foo) { + return Err(ConversionError::WrongObjectKind { + expected_kind: stringify!(Foo).to_string(), + send_kind: object_kind.to_string(), + }); + } + let current_object_version = Self::from_api_version(object_version) + .map_err(|err| ConversionError::ParseCurrentResourceVersion { + source: err, + version: object_version.to_string(), + })?; + match (¤t_object_version, &desired_object_version) { + (Self::V1Alpha1, Self::V1Alpha1) => { + let resource_spec: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1alpha1"), + conversion.steps = 0usize, + "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Alpha1, Self::V1) => { + let resource_spec: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Alpha1, Self::V2Alpha1) => { + let resource_spec: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1::FooSpec = resource_spec.into(); + let resource_spec: v2alpha1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v2alpha1"), + conversion.steps = 2usize, + "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1Alpha1) => { + let resource_spec: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1alpha1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1alpha1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1) => { + let resource_spec: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + tracing::trace!( + from = stringify!(v1), to = stringify!("v1"), conversion + .steps = 0usize, "Successfully converted {type} object", type + = stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V2Alpha1) => { + let resource_spec: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v2alpha1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v2alpha1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V2Alpha1, Self::V1Alpha1) => { + let resource_spec: v2alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1::FooSpec = resource_spec.into(); + let resource_spec: v1alpha1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v1alpha1"), + conversion.steps = 2usize, + "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V2Alpha1, Self::V1) => { + let resource_spec: v2alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource_spec: v1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V2Alpha1, Self::V2Alpha1) => { + let resource_spec: v2alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v2alpha1"), + conversion.steps = 0usize, + "Successfully converted {type} object", type = + stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + } + } + Ok(converted) + } + } pub enum Bar { V1Alpha1, V1, @@ -252,6 +684,39 @@ pub(crate) mod versioned { } } } + /// Parses the version, such as `v1alpha1` + impl ::std::str::FromStr for Bar { + type Err = stackable_versioned::ParseResourceVersionError; + fn from_str(version: &str) -> Result { + match version { + "v1alpha1" => Ok(Self::V1Alpha1), + "v1" => Ok(Self::V1), + "v2alpha1" => Ok(Self::V2Alpha1), + _ => { + Err(stackable_versioned::ParseResourceVersionError::UnknownResourceVersion { + version: version.to_string(), + }) + } + } + } + } + /// Parses the entire `apiVersion`, such as `zookeeper.stackable.tech/v1alpha1`. + impl Bar { + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "bar.example.org/v1alpha1" => Ok(Self::V1Alpha1), + "bar.example.org/v1" => Ok(Self::V1), + "bar.example.org/v2alpha1" => Ok(Self::V2Alpha1), + _ => { + Err(stackable_versioned::ParseResourceVersionError::UnknownApiVersion { + api_version: api_version.to_string(), + }) + } + } + } + } impl Bar { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( @@ -270,4 +735,403 @@ pub(crate) mod versioned { ) } } + #[automatically_derived] + impl Bar { + #[tracing::instrument( + skip_all, + fields( + conversion.kind = review.types.kind, + conversion.api_version = review.types.api_version, + ) + )] + pub fn convert( + review: ::kube::core::conversion::ConversionReview, + ) -> ::kube::core::conversion::ConversionReview { + use ::kube::core::conversion::{ConversionRequest, ConversionResponse}; + use ::kube::core::response::StatusSummary; + use stackable_versioned::ConversionError; + let request = match ConversionRequest::from_review(review) { + Ok(request) => request, + Err(err) => { + tracing::warn!( + ? err, + "Invalid ConversionReview send by Kubernetes apiserver. It probably did not include a request" + ); + return ConversionResponse::invalid(kube::client::Status { + status: Some(StatusSummary::Failure), + code: 400, + message: format!( + "The ConversionReview send did not include any request: {err}" + ), + reason: "ConversionReview request missing".to_string(), + details: None, + }) + .into_review(); + } + }; + let converted = Self::try_convert(&request); + let conversion_response = ConversionResponse::for_request(request); + match converted { + Ok(converted) => { + tracing::debug!( + "Successfully converted {num} objects of type {type}", num = + converted.len(), type = stringify!(Bar), + ); + conversion_response.success(converted).into_review() + } + Err(err) => { + let error_message = err.as_human_readable_error_message(); + conversion_response + .failure(kube::client::Status { + status: Some(StatusSummary::Failure), + code: err.http_return_code(), + message: error_message.clone(), + reason: error_message, + details: None, + }) + .into_review() + } + } + } + #[tracing::instrument(skip_all, err)] + fn try_convert( + request: &::kube::core::conversion::ConversionRequest, + ) -> Result, stackable_versioned::ConversionError> { + use stackable_versioned::ConversionError; + let desired_object_version = Self::from_api_version( + &request.desired_api_version, + ) + .map_err(|err| ConversionError::ParseDesiredResourceVersion { + source: err, + version: request.desired_api_version.to_string(), + })?; + let mut converted: Vec = Vec::with_capacity( + request.objects.len(), + ); + for object in &request.objects { + let object_spec = object + .get("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })?; + let object_kind = object + .get("kind") + .ok_or_else(|| ConversionError::ObjectHasNoKind { + })?; + let object_kind = object_kind + .as_str() + .ok_or_else(|| ConversionError::ObjectKindNotString { + kind: object_kind.clone(), + })?; + let object_version = object + .get("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })?; + let object_version = object_version + .as_str() + .ok_or_else(|| ConversionError::ObjectApiVersionNotString { + api_version: object_version.clone(), + })?; + if object_kind != stringify!(Bar) { + return Err(ConversionError::WrongObjectKind { + expected_kind: stringify!(Bar).to_string(), + send_kind: object_kind.to_string(), + }); + } + let current_object_version = Self::from_api_version(object_version) + .map_err(|err| ConversionError::ParseCurrentResourceVersion { + source: err, + version: object_version.to_string(), + })?; + match (¤t_object_version, &desired_object_version) { + (Self::V1Alpha1, Self::V1Alpha1) => { + let resource_spec: v1alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1alpha1"), + conversion.steps = 0usize, + "Successfully converted {type} object", type = + stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Alpha1, Self::V1) => { + let resource_spec: v1alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource_spec: v1::BarSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Alpha1, Self::V2Alpha1) => { + let resource_spec: v1alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource_spec: v1::BarSpec = resource_spec.into(); + let resource_spec: v2alpha1::BarSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v2alpha1"), + conversion.steps = 2usize, + "Successfully converted {type} object", type = + stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1Alpha1) => { + let resource_spec: v1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource_spec: v1alpha1::BarSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1alpha1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1) => { + let resource_spec: v1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + tracing::trace!( + from = stringify!(v1), to = stringify!("v1"), conversion + .steps = 0usize, "Successfully converted {type} object", type + = stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V2Alpha1) => { + let resource_spec: v1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource_spec: v2alpha1::BarSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v2alpha1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V2Alpha1, Self::V1Alpha1) => { + let resource_spec: v2alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource_spec: v1::BarSpec = resource_spec.into(); + let resource_spec: v1alpha1::BarSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v1alpha1"), + conversion.steps = 2usize, + "Successfully converted {type} object", type = + stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V2Alpha1, Self::V1) => { + let resource_spec: v2alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource_spec: v1::BarSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V2Alpha1, Self::V2Alpha1) => { + let resource_spec: v2alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v2alpha1"), + conversion.steps = 0usize, + "Successfully converted {type} object", type = + stringify!(Bar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + } + } + Ok(converted) + } + } } diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@renamed_kind.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@renamed_kind.rs.snap index fbda4713a..ef09353a1 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@renamed_kind.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@renamed_kind.rs.snap @@ -111,6 +111,41 @@ impl ::std::fmt::Display for FooBar { } } } +/// Parses the version, such as `v1alpha1` +#[automatically_derived] +impl ::std::str::FromStr for FooBar { + type Err = stackable_versioned::ParseResourceVersionError; + fn from_str(version: &str) -> Result { + match version { + "v1alpha1" => Ok(Self::V1Alpha1), + "v1beta1" => Ok(Self::V1Beta1), + "v1" => Ok(Self::V1), + _ => { + Err(stackable_versioned::ParseResourceVersionError::UnknownResourceVersion { + version: version.to_string(), + }) + } + } + } +} +/// Parses the entire `apiVersion`, such as `zookeeper.stackable.tech/v1alpha1`. +#[automatically_derived] +impl FooBar { + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "stackable.tech/v1alpha1" => Ok(Self::V1Alpha1), + "stackable.tech/v1beta1" => Ok(Self::V1Beta1), + "stackable.tech/v1" => Ok(Self::V1), + _ => { + Err(stackable_versioned::ParseResourceVersionError::UnknownApiVersion { + api_version: api_version.to_string(), + }) + } + } + } +} #[automatically_derived] impl FooBar { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. @@ -130,3 +165,396 @@ impl FooBar { ) } } +#[automatically_derived] +impl FooBar { + #[tracing::instrument( + skip_all, + fields( + conversion.kind = review.types.kind, + conversion.api_version = review.types.api_version, + ) + )] + pub fn convert( + review: ::kube::core::conversion::ConversionReview, + ) -> ::kube::core::conversion::ConversionReview { + use ::kube::core::conversion::{ConversionRequest, ConversionResponse}; + use ::kube::core::response::StatusSummary; + use stackable_versioned::ConversionError; + let request = match ConversionRequest::from_review(review) { + Ok(request) => request, + Err(err) => { + tracing::warn!( + ? err, + "Invalid ConversionReview send by Kubernetes apiserver. It probably did not include a request" + ); + return ConversionResponse::invalid(kube::client::Status { + status: Some(StatusSummary::Failure), + code: 400, + message: format!( + "The ConversionReview send did not include any request: {err}" + ), + reason: "ConversionReview request missing".to_string(), + details: None, + }) + .into_review(); + } + }; + let converted = Self::try_convert(&request); + let conversion_response = ConversionResponse::for_request(request); + match converted { + Ok(converted) => { + tracing::debug!( + "Successfully converted {num} objects of type {type}", num = + converted.len(), type = stringify!(FooBar), + ); + conversion_response.success(converted).into_review() + } + Err(err) => { + let error_message = err.as_human_readable_error_message(); + conversion_response + .failure(kube::client::Status { + status: Some(StatusSummary::Failure), + code: err.http_return_code(), + message: error_message.clone(), + reason: error_message, + details: None, + }) + .into_review() + } + } + } + #[tracing::instrument(skip_all, err)] + fn try_convert( + request: &::kube::core::conversion::ConversionRequest, + ) -> Result, stackable_versioned::ConversionError> { + use stackable_versioned::ConversionError; + let desired_object_version = Self::from_api_version(&request.desired_api_version) + .map_err(|err| ConversionError::ParseDesiredResourceVersion { + source: err, + version: request.desired_api_version.to_string(), + })?; + let mut converted: Vec = Vec::with_capacity( + request.objects.len(), + ); + for object in &request.objects { + let object_spec = object + .get("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })?; + let object_kind = object + .get("kind") + .ok_or_else(|| ConversionError::ObjectHasNoKind { + })?; + let object_kind = object_kind + .as_str() + .ok_or_else(|| ConversionError::ObjectKindNotString { + kind: object_kind.clone(), + })?; + let object_version = object + .get("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })?; + let object_version = object_version + .as_str() + .ok_or_else(|| ConversionError::ObjectApiVersionNotString { + api_version: object_version.clone(), + })?; + if object_kind != stringify!(FooBar) { + return Err(ConversionError::WrongObjectKind { + expected_kind: stringify!(FooBar).to_string(), + send_kind: object_kind.to_string(), + }); + } + let current_object_version = Self::from_api_version(object_version) + .map_err(|err| ConversionError::ParseCurrentResourceVersion { + source: err, + version: object_version.to_string(), + })?; + match (¤t_object_version, &desired_object_version) { + (Self::V1Alpha1, Self::V1Alpha1) => { + let resource_spec: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1alpha1"), + conversion.steps = 0usize, + "Successfully converted {type} object", type = + stringify!(FooBar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Alpha1, Self::V1Beta1) => { + let resource_spec: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + let resource_spec: v1beta1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1beta1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(FooBar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Alpha1, Self::V1) => { + let resource_spec: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + let resource_spec: v1beta1::FooSpec = resource_spec.into(); + let resource_spec: v1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1"), conversion + .steps = 2usize, "Successfully converted {type} object", type = + stringify!(FooBar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Beta1, Self::V1Alpha1) => { + let resource_spec: v1beta1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + let resource_spec: v1alpha1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1beta1), to = stringify!("v1alpha1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(FooBar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Beta1, Self::V1Beta1) => { + let resource_spec: v1beta1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + tracing::trace!( + from = stringify!(v1beta1), to = stringify!("v1beta1"), + conversion.steps = 0usize, + "Successfully converted {type} object", type = + stringify!(FooBar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1Beta1, Self::V1) => { + let resource_spec: v1beta1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + let resource_spec: v1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1beta1), to = stringify!("v1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(FooBar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1Alpha1) => { + let resource_spec: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + let resource_spec: v1beta1::FooSpec = resource_spec.into(); + let resource_spec: v1alpha1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1alpha1"), conversion + .steps = 2usize, "Successfully converted {type} object", type = + stringify!(FooBar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1Beta1) => { + let resource_spec: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + let resource_spec: v1beta1::FooSpec = resource_spec.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1beta1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(FooBar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + (Self::V1, Self::V1) => { + let resource_spec: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + tracing::trace!( + from = stringify!(v1), to = stringify!("v1"), conversion.steps = + 0usize, "Successfully converted {type} object", type = + stringify!(FooBar), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + } + } + Ok(converted) + } +} diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@shortnames.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@shortnames.rs.snap index b92e44ecb..51b430749 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@shortnames.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@shortnames.rs.snap @@ -38,6 +38,37 @@ impl ::std::fmt::Display for Foo { } } } +/// Parses the version, such as `v1alpha1` +#[automatically_derived] +impl ::std::str::FromStr for Foo { + type Err = stackable_versioned::ParseResourceVersionError; + fn from_str(version: &str) -> Result { + match version { + "v1alpha1" => Ok(Self::V1Alpha1), + _ => { + Err(stackable_versioned::ParseResourceVersionError::UnknownResourceVersion { + version: version.to_string(), + }) + } + } + } +} +/// Parses the entire `apiVersion`, such as `zookeeper.stackable.tech/v1alpha1`. +#[automatically_derived] +impl Foo { + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "stackable.tech/v1alpha1" => Ok(Self::V1Alpha1), + _ => { + Err(stackable_versioned::ParseResourceVersionError::UnknownApiVersion { + api_version: api_version.to_string(), + }) + } + } + } +} #[automatically_derived] impl Foo { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. @@ -53,3 +84,144 @@ impl Foo { ) } } +#[automatically_derived] +impl Foo { + #[tracing::instrument( + skip_all, + fields( + conversion.kind = review.types.kind, + conversion.api_version = review.types.api_version, + ) + )] + pub fn convert( + review: ::kube::core::conversion::ConversionReview, + ) -> ::kube::core::conversion::ConversionReview { + use ::kube::core::conversion::{ConversionRequest, ConversionResponse}; + use ::kube::core::response::StatusSummary; + use stackable_versioned::ConversionError; + let request = match ConversionRequest::from_review(review) { + Ok(request) => request, + Err(err) => { + tracing::warn!( + ? err, + "Invalid ConversionReview send by Kubernetes apiserver. It probably did not include a request" + ); + return ConversionResponse::invalid(kube::client::Status { + status: Some(StatusSummary::Failure), + code: 400, + message: format!( + "The ConversionReview send did not include any request: {err}" + ), + reason: "ConversionReview request missing".to_string(), + details: None, + }) + .into_review(); + } + }; + let converted = Self::try_convert(&request); + let conversion_response = ConversionResponse::for_request(request); + match converted { + Ok(converted) => { + tracing::debug!( + "Successfully converted {num} objects of type {type}", num = + converted.len(), type = stringify!(Foo), + ); + conversion_response.success(converted).into_review() + } + Err(err) => { + let error_message = err.as_human_readable_error_message(); + conversion_response + .failure(kube::client::Status { + status: Some(StatusSummary::Failure), + code: err.http_return_code(), + message: error_message.clone(), + reason: error_message, + details: None, + }) + .into_review() + } + } + } + #[tracing::instrument(skip_all, err)] + fn try_convert( + request: &::kube::core::conversion::ConversionRequest, + ) -> Result, stackable_versioned::ConversionError> { + use stackable_versioned::ConversionError; + let desired_object_version = Self::from_api_version(&request.desired_api_version) + .map_err(|err| ConversionError::ParseDesiredResourceVersion { + source: err, + version: request.desired_api_version.to_string(), + })?; + let mut converted: Vec = Vec::with_capacity( + request.objects.len(), + ); + for object in &request.objects { + let object_spec = object + .get("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })?; + let object_kind = object + .get("kind") + .ok_or_else(|| ConversionError::ObjectHasNoKind { + })?; + let object_kind = object_kind + .as_str() + .ok_or_else(|| ConversionError::ObjectKindNotString { + kind: object_kind.clone(), + })?; + let object_version = object + .get("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })?; + let object_version = object_version + .as_str() + .ok_or_else(|| ConversionError::ObjectApiVersionNotString { + api_version: object_version.clone(), + })?; + if object_kind != stringify!(Foo) { + return Err(ConversionError::WrongObjectKind { + expected_kind: stringify!(Foo).to_string(), + send_kind: object_kind.to_string(), + }); + } + let current_object_version = Self::from_api_version(object_version) + .map_err(|err| ConversionError::ParseCurrentResourceVersion { + source: err, + version: object_version.to_string(), + })?; + match (¤t_object_version, &desired_object_version) { + (Self::V1Alpha1, Self::V1Alpha1) => { + let resource_spec: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1alpha1"), + conversion.steps = 0usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + let mut object = object.clone(); + *object + .get_mut("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + *object + .get_mut("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })? = serde_json::Value::String( + request.desired_api_version.clone(), + ); + converted.push(object); + } + } + } + Ok(converted) + } +} diff --git a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@skip.rs.snap b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@skip.rs.snap index 39f0b2263..7055f4505 100644 --- a/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@skip.rs.snap +++ b/crates/stackable-versioned-macros/fixtures/snapshots/stackable_versioned_macros__test__k8s_snapshots@skip.rs.snap @@ -92,3 +92,113 @@ pub mod v1 { pub baz: bool, } } +#[automatically_derived] +impl Foo { + #[tracing::instrument( + skip_all, + fields( + conversion.kind = review.types.kind, + conversion.api_version = review.types.api_version, + ) + )] + pub fn convert( + review: ::kube::core::conversion::ConversionReview, + ) -> ::kube::core::conversion::ConversionReview { + use ::kube::core::conversion::{ConversionRequest, ConversionResponse}; + use ::kube::core::response::StatusSummary; + use stackable_versioned::ConversionError; + let request = match ConversionRequest::from_review(review) { + Ok(request) => request, + Err(err) => { + tracing::warn!( + ? err, + "Invalid ConversionReview send by Kubernetes apiserver. It probably did not include a request" + ); + return ConversionResponse::invalid(kube::client::Status { + status: Some(StatusSummary::Failure), + code: 400, + message: format!( + "The ConversionReview send did not include any request: {err}" + ), + reason: "ConversionReview request missing".to_string(), + details: None, + }) + .into_review(); + } + }; + let converted = Self::try_convert(&request); + let conversion_response = ConversionResponse::for_request(request); + match converted { + Ok(converted) => { + tracing::debug!( + "Successfully converted {num} objects of type {type}", num = + converted.len(), type = stringify!(Foo), + ); + conversion_response.success(converted).into_review() + } + Err(err) => { + let error_message = err.as_human_readable_error_message(); + conversion_response + .failure(kube::client::Status { + status: Some(StatusSummary::Failure), + code: err.http_return_code(), + message: error_message.clone(), + reason: error_message, + details: None, + }) + .into_review() + } + } + } + #[tracing::instrument(skip_all, err)] + fn try_convert( + request: &::kube::core::conversion::ConversionRequest, + ) -> Result, stackable_versioned::ConversionError> { + use stackable_versioned::ConversionError; + let desired_object_version = Self::from_api_version(&request.desired_api_version) + .map_err(|err| ConversionError::ParseDesiredResourceVersion { + source: err, + version: request.desired_api_version.to_string(), + })?; + let mut converted: Vec = Vec::with_capacity( + request.objects.len(), + ); + for object in &request.objects { + let object_spec = object + .get("spec") + .ok_or_else(|| ConversionError::ObjectHasNoSpec { + })?; + let object_kind = object + .get("kind") + .ok_or_else(|| ConversionError::ObjectHasNoKind { + })?; + let object_kind = object_kind + .as_str() + .ok_or_else(|| ConversionError::ObjectKindNotString { + kind: object_kind.clone(), + })?; + let object_version = object + .get("apiVersion") + .ok_or_else(|| ConversionError::ObjectHasNoApiVersion { + })?; + let object_version = object_version + .as_str() + .ok_or_else(|| ConversionError::ObjectApiVersionNotString { + api_version: object_version.clone(), + })?; + if object_kind != stringify!(Foo) { + return Err(ConversionError::WrongObjectKind { + expected_kind: stringify!(Foo).to_string(), + send_kind: object_kind.to_string(), + }); + } + let current_object_version = Self::from_api_version(object_version) + .map_err(|err| ConversionError::ParseCurrentResourceVersion { + source: err, + version: object_version.to_string(), + })?; + match (¤t_object_version, &desired_object_version) {} + } + Ok(converted) + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/container/mod.rs b/crates/stackable-versioned-macros/src/codegen/container/mod.rs index fbbff4006..f648d42d3 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/mod.rs @@ -84,11 +84,11 @@ impl Container { } } - /// Generates Kubernetes specific code to merge two or more CRDs into one. + /// Generates Kubernetes specific code to merge two CRDs or convert between different versions. /// /// This function only returns `Some` if it is a struct. Enums cannot be used to define /// Kubernetes custom resources. - pub(crate) fn generate_kubernetes_merge_crds( + pub(crate) fn generate_kubernetes_code( &self, enum_variant_idents: &[IdentString], enum_variant_strings: &[String], @@ -96,16 +96,34 @@ impl Container { vis: &Visibility, is_nested: bool, ) -> Option { - match self { - Container::Struct(s) => s.generate_kubernetes_merge_crds( + let Container::Struct(s) = self else { + return None; + }; + let kubernetes_options = s.common.options.kubernetes_options.as_ref()?; + + let mut tokens = TokenStream::new(); + + if !kubernetes_options.skip_merged_crd { + tokens.extend(s.generate_kubernetes_merge_crds( enum_variant_idents, enum_variant_strings, fn_calls, vis, is_nested, - ), - Container::Enum(_) => None, + )); } + + #[cfg(feature = "flux-converter")] + // TODO: Do we need a kubernetes_options.skip_conversion as well? + tokens.extend(super::flux_converter::generate_kubernetes_conversion( + &s.common.idents.kubernetes, + &s.common.idents.original, + enum_variant_idents, + enum_variant_strings, + kubernetes_options, + )); + + Some(tokens) } pub(crate) fn get_original_ident(&self) -> &Ident { @@ -214,7 +232,7 @@ impl StandaloneContainer { }); } - tokens.extend(self.container.generate_kubernetes_merge_crds( + tokens.extend(self.container.generate_kubernetes_code( &kubernetes_enum_variant_idents, &kubernetes_enum_variant_strings, &kubernetes_merge_crds_fn_calls, diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct.rs b/crates/stackable-versioned-macros/src/codegen/container/struct.rs index 584a293b1..64c22b765 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct.rs @@ -349,9 +349,15 @@ impl Struct { vis: &Visibility, is_nested: bool, ) -> Option { + assert_eq!(enum_variant_idents.len(), enum_variant_strings.len()); + match &self.common.options.kubernetes_options { Some(kubernetes_options) if !kubernetes_options.skip_merged_crd => { + let k8s_group = &kubernetes_options.group; let enum_ident = &self.common.idents.kubernetes; + let api_versions = enum_variant_strings + .iter() + .map(|version| format!("{k8s_group}/{version}")); // Only add the #[automatically_derived] attribute if this impl is used outside of a // module (in standalone mode). @@ -362,6 +368,9 @@ impl Struct { let k8s_openapi_path = &*kubernetes_options.crates.k8s_openapi; let kube_core_path = &*kubernetes_options.crates.kube_core; + // FIXME + let versioned_path = quote! { stackable_versioned }; + Some(quote! { #automatically_derived #vis enum #enum_ident { @@ -377,12 +386,39 @@ impl Struct { } } + /// Parses the version, such as `v1alpha1` + #automatically_derived + impl ::std::str::FromStr for #enum_ident { + type Err = #versioned_path::ParseResourceVersionError; + + fn from_str(version: &str) -> Result { + match version { + #(#enum_variant_strings => Ok(Self::#enum_variant_idents),)* + _ => Err(#versioned_path::ParseResourceVersionError::UnknownResourceVersion{version: version.to_string()}), + } + } + } + + /// Parses the entire `apiVersion`, such as `zookeeper.stackable.tech/v1alpha1`. + #automatically_derived + impl #enum_ident { + pub fn from_api_version(api_version: &str) -> Result { + match api_version { + #(#api_versions => Ok(Self::#enum_variant_idents),)* + _ => Err(#versioned_path::ParseResourceVersionError::UnknownApiVersion{api_version: api_version.to_string()}), + } + } + } + #automatically_derived impl #enum_ident { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( stored_apiversion: Self - ) -> ::std::result::Result<#k8s_openapi_path::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, #kube_core_path::crd::MergeError> { + ) -> ::std::result::Result< + #k8s_openapi_path::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, + #kube_core_path::crd::MergeError + > { #kube_core_path::crd::merge_crds(vec![#(#fn_calls),*], &stored_apiversion.to_string()) } } diff --git a/crates/stackable-versioned-macros/src/codegen/flux_converter.rs b/crates/stackable-versioned-macros/src/codegen/flux_converter.rs new file mode 100644 index 000000000..3228c0628 --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/flux_converter.rs @@ -0,0 +1,236 @@ +use darling::util::IdentString; +use proc_macro2::TokenStream; +use quote::quote; + +use super::container::KubernetesOptions; + +pub(crate) fn generate_kubernetes_conversion( + enum_ident: &IdentString, + struct_ident: &IdentString, + enum_variant_idents: &[IdentString], + enum_variant_strings: &[String], + kubernetes_options: &KubernetesOptions, +) -> Option { + assert_eq!(enum_variant_idents.len(), enum_variant_strings.len()); + + // Get the crate paths + let kube_core_path = &*kubernetes_options.crates.kube_core; + // FIXME + let kube_path = quote! {kube}; + let versioned_path = quote! {stackable_versioned}; + + let versions = enum_variant_idents + .iter() + .zip(enum_variant_strings) + .collect::>(); + let conversion_chain = generate_conversion_chain(versions); + + let matches = conversion_chain.into_iter().map( + |((src, src_lower), (dst, dst_lower), version_chain)| { + let steps = version_chain.len(); + let version_chain_string = version_chain.iter() + .map(|(_,v)| v.parse::() + .expect("The versions always needs to be a valid TokenStream")); + + // TODO: Is there a bit more clever way how we can get this? + let src_lower = src_lower.parse::().expect("The versions always needs to be a valid TokenStream"); + + quote! { (Self::#src, Self::#dst) => { + let resource_spec: #src_lower::#struct_ident = serde_json::from_value(object_spec.clone()) + .map_err(|err| ConversionError::DeserializeObjectSpec{source: err, kind: stringify!(#enum_ident).to_string()})?; + + #( + let resource_spec: #version_chain_string::#struct_ident = resource_spec.into(); + )* + + tracing::trace!( + from = stringify!(#src_lower), + to = stringify!(#dst_lower), + conversion.steps = #steps, + "Successfully converted {type} object", + type = stringify!(#enum_ident), + ); + + let mut object = object.clone(); + *object.get_mut("spec").ok_or_else(|| ConversionError::ObjectHasNoSpec{})? = serde_json::to_value(resource_spec) + .map_err(|err| ConversionError::SerializeObjectSpec{source: err, kind: stringify!(#enum_ident).to_string()})?; + *object.get_mut("apiVersion").ok_or_else(|| ConversionError::ObjectHasNoApiVersion{})? + = serde_json::Value::String(request.desired_api_version.clone()); + converted.push(object); + }} + }, + ); + + Some(quote! { + #[automatically_derived] + impl #enum_ident { + #[tracing::instrument( + skip_all, + fields( + conversion.kind = review.types.kind, + conversion.api_version = review.types.api_version, + ) + )] + pub fn convert(review: #kube_core_path::conversion::ConversionReview) -> #kube_core_path::conversion::ConversionReview { + // Intentionally not using `snafu::ResultExt` here to keep the number of dependencies minimal + use #kube_core_path::conversion::{ConversionRequest, ConversionResponse}; + use #kube_core_path::response::StatusSummary; + use #versioned_path::ConversionError; + + let request = match ConversionRequest::from_review(review) { + Ok(request) => request, + Err(err) => { + tracing::warn!( + ?err, + "Invalid ConversionReview send by Kubernetes apiserver. It probably did not include a request" + ); + + return ConversionResponse::invalid( + #kube_path::client::Status { + status: Some(StatusSummary::Failure), + code: 400, + message: format!("The ConversionReview send did not include any request: {err}"), + reason: "ConversionReview request missing".to_string(), + details: None, + }, + ).into_review(); + } + }; + + let converted = Self::try_convert(&request); + + let conversion_response = ConversionResponse::for_request(request); + match converted { + Ok(converted) => { + tracing::debug!( + "Successfully converted {num} objects of type {type}", + num = converted.len(), + type = stringify!(#enum_ident), + ); + + conversion_response.success(converted).into_review() + }, + Err(err) => { + let error_message = err.as_human_readable_error_message(); + + conversion_response.failure( + #kube_path::client::Status { + status: Some(StatusSummary::Failure), + code: err.http_return_code(), + message: error_message.clone(), + reason: error_message, + details: None, + }, + ).into_review() + } + } + } + + #[tracing::instrument( + skip_all, + err + )] + fn try_convert(request: &#kube_core_path::conversion::ConversionRequest) -> Result, #versioned_path::ConversionError> { + use #versioned_path::ConversionError; + + // FIXME: Check that request.types.{kind,api_version} match the expected values + + let desired_object_version = Self::from_api_version(&request.desired_api_version) + .map_err(|err| ConversionError::ParseDesiredResourceVersion{ + source: err, + version: request.desired_api_version.to_string() + })?; + + let mut converted: Vec = Vec::with_capacity(request.objects.len()); + for object in &request.objects { + let object_spec = object.get("spec").ok_or_else(|| ConversionError::ObjectHasNoSpec{})?; + let object_kind = object.get("kind").ok_or_else(|| ConversionError::ObjectHasNoKind{})?; + let object_kind = object_kind.as_str().ok_or_else(|| ConversionError::ObjectKindNotString{kind: object_kind.clone()})?; + let object_version = object.get("apiVersion").ok_or_else(|| ConversionError::ObjectHasNoApiVersion{})?; + let object_version = object_version.as_str().ok_or_else(|| ConversionError::ObjectApiVersionNotString{api_version: object_version.clone()})?; + + if object_kind != stringify!(#enum_ident) { + return Err(ConversionError::WrongObjectKind{expected_kind: stringify!(#enum_ident).to_string(), send_kind: object_kind.to_string()}); + } + + let current_object_version = Self::from_api_version(object_version) + .map_err(|err| ConversionError::ParseCurrentResourceVersion{ + source: err, + version: object_version.to_string() + })?; + + match (¤t_object_version, &desired_object_version) { + #(#matches),* + } + } + + Ok(converted) + } + } + }) +} + +pub fn generate_conversion_chain( + versions: Vec, +) -> Vec<(Version, Version, Vec)> { + let mut result = Vec::with_capacity(versions.len().pow(2)); + let n = versions.len(); + + for i in 0..n { + for j in 0..n { + let source = versions[i].clone(); + let destination = versions[j].clone(); + let chain = if i == j { + vec![] + } else if i < j { + versions[i + 1..=j].to_vec() + } else { + versions[j..i].iter().rev().cloned().collect() + }; + result.push((source, destination, chain)); + } + } + + result +} + +#[cfg(test)] +mod tests { + use super::generate_conversion_chain; + + #[test] + fn test_generate_conversion_chains() { + let versions = vec!["v1alpha1", "v1alpha2", "v1beta1", "v1", "v2"]; + let conversion_chain = generate_conversion_chain(versions); + + assert_eq!(conversion_chain, vec![ + ("v1alpha1", "v1alpha1", vec![]), + ("v1alpha1", "v1alpha2", vec!["v1alpha2"]), + ("v1alpha1", "v1beta1", vec!["v1alpha2", "v1beta1"]), + ("v1alpha1", "v1", vec!["v1alpha2", "v1beta1", "v1"]), + ("v1alpha1", "v2", vec!["v1alpha2", "v1beta1", "v1", "v2"]), + ("v1alpha2", "v1alpha1", vec!["v1alpha1"]), + ("v1alpha2", "v1alpha2", vec![]), + ("v1alpha2", "v1beta1", vec!["v1beta1"]), + ("v1alpha2", "v1", vec!["v1beta1", "v1"]), + ("v1alpha2", "v2", vec!["v1beta1", "v1", "v2"]), + ("v1beta1", "v1alpha1", vec!["v1alpha2", "v1alpha1"]), + ("v1beta1", "v1alpha2", vec!["v1alpha2"]), + ("v1beta1", "v1beta1", vec![]), + ("v1beta1", "v1", vec!["v1"]), + ("v1beta1", "v2", vec!["v1", "v2"]), + ("v1", "v1alpha1", vec!["v1beta1", "v1alpha2", "v1alpha1"]), + ("v1", "v1alpha2", vec!["v1beta1", "v1alpha2"]), + ("v1", "v1beta1", vec!["v1beta1"]), + ("v1", "v1", vec![]), + ("v1", "v2", vec!["v2"]), + ("v2", "v1alpha1", vec![ + "v1", "v1beta1", "v1alpha2", "v1alpha1" + ]), + ("v2", "v1alpha2", vec!["v1", "v1beta1", "v1alpha2"]), + ("v2", "v1beta1", vec!["v1", "v1beta1"]), + ("v2", "v1", vec!["v1"]), + ("v2", "v2", vec![]) + ]); + } +} diff --git a/crates/stackable-versioned-macros/src/codegen/mod.rs b/crates/stackable-versioned-macros/src/codegen/mod.rs index 4f4b2ea3c..e8fb2bdfa 100644 --- a/crates/stackable-versioned-macros/src/codegen/mod.rs +++ b/crates/stackable-versioned-macros/src/codegen/mod.rs @@ -10,6 +10,9 @@ pub(crate) mod container; pub(crate) mod item; pub(crate) mod module; +#[cfg(feature = "flux-converter")] +pub(crate) mod flux_converter; + #[derive(Debug)] pub(crate) struct VersionDefinition { /// Indicates that the container version is deprecated. diff --git a/crates/stackable-versioned-macros/src/codegen/module.rs b/crates/stackable-versioned-macros/src/codegen/module.rs index 217dacced..22581578a 100644 --- a/crates/stackable-versioned-macros/src/codegen/module.rs +++ b/crates/stackable-versioned-macros/src/codegen/module.rs @@ -221,7 +221,7 @@ impl Module { kubernetes_enum_variant_strings, )) = kubernetes_container_items.get(container.get_original_ident()) { - kubernetes_tokens.extend(container.generate_kubernetes_merge_crds( + kubernetes_tokens.extend(container.generate_kubernetes_code( kubernetes_enum_variant_idents, kubernetes_enum_variant_strings, kubernetes_merge_crds_fn_calls, diff --git a/crates/stackable-versioned/Cargo.toml b/crates/stackable-versioned/Cargo.toml index 9f4327f31..823d04502 100644 --- a/crates/stackable-versioned/Cargo.toml +++ b/crates/stackable-versioned/Cargo.toml @@ -11,10 +11,33 @@ repository.workspace = true all-features = true [features] -full = ["k8s"] +full = ["k8s", "flux-converter"] k8s = [ "stackable-versioned-macros/k8s", # Forward the k8s feature to the underlying macro crate + "dep:kube", + "dep:k8s-openapi", +] +flux-converter = [ + "k8s", + "stackable-versioned-macros/flux-converter", + "dep:kube", + "dep:k8s-openapi", + "dep:serde", + "dep:schemars", + "dep:serde_json", + "dep:tracing" ] [dependencies] stackable-versioned-macros = { path = "../stackable-versioned-macros" } + +kube = { workspace = true, optional = true } +k8s-openapi = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +schemars = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } +snafu.workspace = true +tracing = { workspace = true, optional = true } + +[dev-dependencies] +insta.workspace = true diff --git a/crates/stackable-versioned/fixtures/inputs/fail/request_missing.json b/crates/stackable-versioned/fixtures/inputs/fail/request_missing.json new file mode 100644 index 000000000..b5759bcdb --- /dev/null +++ b/crates/stackable-versioned/fixtures/inputs/fail/request_missing.json @@ -0,0 +1,4 @@ +{ + "kind": "ConversionReview", + "apiVersion": "apiextensions.k8s.io/v1" +} diff --git a/crates/stackable-versioned/fixtures/inputs/fail/undeserializable_missing_field.json b/crates/stackable-versioned/fixtures/inputs/fail/undeserializable_missing_field.json new file mode 100644 index 000000000..56b8c5c8d --- /dev/null +++ b/crates/stackable-versioned/fixtures/inputs/fail/undeserializable_missing_field.json @@ -0,0 +1,20 @@ +{ + "kind": "ConversionReview", + "apiVersion": "apiextensions.k8s.io/v1", + "request": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "desiredAPIVersion": "test.stackable.tech/v3", + "objects": [ + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": {} + } + ] + } +} diff --git a/crates/stackable-versioned/fixtures/inputs/fail/unkown_current_version.json b/crates/stackable-versioned/fixtures/inputs/fail/unkown_current_version.json new file mode 100644 index 000000000..6df3bb2d1 --- /dev/null +++ b/crates/stackable-versioned/fixtures/inputs/fail/unkown_current_version.json @@ -0,0 +1,22 @@ +{ + "kind": "ConversionReview", + "apiVersion": "apiextensions.k8s.io/v1", + "request": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "desiredAPIVersion": "test.stackable.tech/v3", + "objects": [ + { + "apiVersion": "test.stackable.tech/v99", + "kind": "Person", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": { + "username": "sbernauer" + } + } + ] + } +} diff --git a/crates/stackable-versioned/fixtures/inputs/fail/unkown_desired_version.json b/crates/stackable-versioned/fixtures/inputs/fail/unkown_desired_version.json new file mode 100644 index 000000000..736a95dfb --- /dev/null +++ b/crates/stackable-versioned/fixtures/inputs/fail/unkown_desired_version.json @@ -0,0 +1,22 @@ +{ + "kind": "ConversionReview", + "apiVersion": "apiextensions.k8s.io/v1", + "request": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "desiredAPIVersion": "test.stackable.tech/v99", + "objects": [ + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": { + "username": "sbernauer" + } + } + ] + } +} diff --git a/crates/stackable-versioned/fixtures/inputs/fail/wrong_object.json b/crates/stackable-versioned/fixtures/inputs/fail/wrong_object.json new file mode 100644 index 000000000..3c5bbcd67 --- /dev/null +++ b/crates/stackable-versioned/fixtures/inputs/fail/wrong_object.json @@ -0,0 +1,22 @@ +{ + "kind": "ConversionReview", + "apiVersion": "apiextensions.k8s.io/v1", + "request": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "desiredAPIVersion": "test.stackable.tech/v3", + "objects": [ + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "SomeOtherResource", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": { + "username": "sbernauer" + } + } + ] + } +} diff --git a/crates/stackable-versioned/fixtures/inputs/pass/persons_to_v1alpha1.json b/crates/stackable-versioned/fixtures/inputs/pass/persons_to_v1alpha1.json new file mode 100644 index 000000000..89a1d1704 --- /dev/null +++ b/crates/stackable-versioned/fixtures/inputs/pass/persons_to_v1alpha1.json @@ -0,0 +1,60 @@ +{ + "kind": "ConversionReview", + "apiVersion": "apiextensions.k8s.io/v1", + "request": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "desiredAPIVersion": "test.stackable.tech/v1alpha1", + "objects": [ + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": { + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v1alpha2", + "kind": "Person", + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v1beta1", + "kind": "Person", + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v2", + "kind": "Person", + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer", + "gender": "Male" + } + }, + { + "apiVersion": "test.stackable.tech/v3", + "kind": "Person", + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer", + "gender": "Male" + } + } + ] + } +} diff --git a/crates/stackable-versioned/fixtures/inputs/pass/persons_to_v3.json b/crates/stackable-versioned/fixtures/inputs/pass/persons_to_v3.json new file mode 100644 index 000000000..6f1429da9 --- /dev/null +++ b/crates/stackable-versioned/fixtures/inputs/pass/persons_to_v3.json @@ -0,0 +1,60 @@ +{ + "kind": "ConversionReview", + "apiVersion": "apiextensions.k8s.io/v1", + "request": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "desiredAPIVersion": "test.stackable.tech/v3", + "objects": [ + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": { + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v1alpha2", + "kind": "Person", + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v1beta1", + "kind": "Person", + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v2", + "kind": "Person", + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer", + "gender": "Male" + } + }, + { + "apiVersion": "test.stackable.tech/v3", + "kind": "Person", + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer", + "gender": "Male" + } + } + ] + } +} diff --git a/crates/stackable-versioned/src/apply_resource.rs b/crates/stackable-versioned/src/apply_resource.rs new file mode 100644 index 000000000..a82c2077b --- /dev/null +++ b/crates/stackable-versioned/src/apply_resource.rs @@ -0,0 +1,18 @@ +use k8s_openapi::Resource; +use kube::Client; +/// Given a [kube::Client], apply a resource to the server. +/// +/// This is especially useful when you have custom requirements for deploying +/// CRDs to clusters which already have a definition. +/// +/// For example, you want to prevent stable versions (v1) from having any +/// change. + +// FIXME(Nick): Remove unused +#[allow(unused)] +pub trait ApplyResource: Resource { + type Error; + + /// Apply a resource to a cluster + fn apply(&self, kube_client: Client) -> Result<(), Self::Error>; +} diff --git a/crates/stackable-versioned/src/flux_converter/apply_crd.rs b/crates/stackable-versioned/src/flux_converter/apply_crd.rs new file mode 100644 index 000000000..62870358a --- /dev/null +++ b/crates/stackable-versioned/src/flux_converter/apply_crd.rs @@ -0,0 +1,38 @@ +use std::convert::Infallible; + +use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition; + +use crate::apply_resource::ApplyResource; + +impl ApplyResource for CustomResourceDefinition { + type Error = Infallible; + + fn apply(&self, _kube_client: kube::Client) -> Result<(), Self::Error> { + // 1. Using the kube::Client, check if the CRD already exists. + // If it does not exist, then simple apply. + // + // 2. If the CRD already exists, then get it, and check... + // - spec.conversion (this will often change, which is fine (e.g. caBundle rotation)) + // - spec.group (this should never change) + // - spec.names (it is ok to add names, probably not great to remove them, but legit as + // we can only keep a limited number because of CR size limitations) + // - spec.preserve_unknown_fields (we can be opinionated and reject Some(false) + // (and accept None and Some(true)). This is because the field is deprecated in favor + // of setting x-preserve-unknown-fields to true in spec.versions\[*\].schema.openAPIV3Schema. + // See https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#field-pruning + // for details. + // - spec.scope (this should never change) + // + // 3. For spec.versions, where "A" is the set of versions currently defined on the stored CRD, + // and "B" is the set of versions to be applied... + // - A - B: These versions are candidates for removal + // - B - A: These versions can be safely appended + // - A ∩ B: These versions are likely to change in the following ways: + // - New optional fields added (safe for vXalphaY, vXbetaY, and vX) + // - Fields changed (can happen in vXalphaY, vXbetaY, but shouldn't in vX) + // - Fields removed (can happen in vXalphaY, vXbetaY, but shouldn't in vX) + // + // Complete the rest of the owl... + Ok(()) + } +} diff --git a/crates/stackable-versioned/src/flux_converter/mod.rs b/crates/stackable-versioned/src/flux_converter/mod.rs new file mode 100644 index 000000000..033dd407a --- /dev/null +++ b/crates/stackable-versioned/src/flux_converter/mod.rs @@ -0,0 +1,95 @@ +//! `flux-converter` is part of the project DeLorean :) +//! +//! It converts between different CRD versions by using 1.21 GW of power, +//! 142km/h and time travel. + +use std::{error::Error, fmt::Write}; + +use snafu::Snafu; + +use crate::ParseResourceVersionError; + +mod apply_crd; + +#[cfg(test)] +mod tests; + +#[derive(Debug, Snafu)] +pub enum ConversionError { + #[snafu(display("failed to parse current resource version \"{version}\""))] + ParseCurrentResourceVersion { + source: ParseResourceVersionError, + version: String, + }, + + #[snafu(display("failed to parse desired resource version \"{version}\""))] + ParseDesiredResourceVersion { + source: ParseResourceVersionError, + version: String, + }, + + #[snafu(display("the object send for conversion has no \"spec\" field"))] + ObjectHasNoSpec {}, + + #[snafu(display("the object send for conversion has no \"kind\" field"))] + ObjectHasNoKind {}, + + #[snafu(display("the object send for conversion has no \"apiVersion\" field"))] + ObjectHasNoApiVersion {}, + + #[snafu(display("the \"kind\" field of the object send for conversion isn't a String"))] + ObjectKindNotString { kind: serde_json::Value }, + + #[snafu(display("the \"apiVersion\" field of the object send for conversion isn't a String"))] + ObjectApiVersionNotString { api_version: serde_json::Value }, + + #[snafu(display( + "I was asked to convert the kind \"{send_kind}\", but I can only convert objects of kind \"{expected_kind}\"" + ))] + WrongObjectKind { + expected_kind: String, + send_kind: String, + }, + + #[snafu(display("failed to deserialize object of kind \"{kind}\""))] + DeserializeObjectSpec { + source: serde_json::Error, + kind: String, + }, + + #[snafu(display("failed to serialize object of kind \"{kind}\""))] + SerializeObjectSpec { + source: serde_json::Error, + kind: String, + }, +} + +impl ConversionError { + pub fn http_return_code(&self) -> u16 { + match &self { + ConversionError::ParseCurrentResourceVersion { .. } => 500, + ConversionError::ParseDesiredResourceVersion { .. } => 500, + ConversionError::ObjectHasNoSpec {} => 400, + ConversionError::ObjectHasNoKind {} => 400, + ConversionError::ObjectHasNoApiVersion {} => 400, + ConversionError::ObjectKindNotString { .. } => 400, + ConversionError::ObjectApiVersionNotString { .. } => 400, + ConversionError::WrongObjectKind { .. } => 400, + ConversionError::DeserializeObjectSpec { .. } => 500, + ConversionError::SerializeObjectSpec { .. } => 500, + } + } + + pub fn as_human_readable_error_message(&self) -> String { + let mut error_message = String::new(); + write!(error_message, "{self}").expect("Writing to Strings can not fail"); + + let mut source = self.source(); + while let Some(err) = source { + write!(error_message, ": {err}").expect("Writing to Strings can not fail"); + source = err.source(); + } + + error_message + } +} diff --git a/crates/stackable-versioned/src/flux_converter/tests/mod.rs b/crates/stackable-versioned/src/flux_converter/tests/mod.rs new file mode 100644 index 000000000..c30dd3d25 --- /dev/null +++ b/crates/stackable-versioned/src/flux_converter/tests/mod.rs @@ -0,0 +1,182 @@ +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use stackable_versioned_macros::versioned; + +use crate as stackable_versioned; + +#[versioned( + k8s(group = "test.stackable.tech",), + version(name = "v1alpha1"), + version(name = "v1alpha2"), + version(name = "v1beta1"), + version(name = "v2"), + version(name = "v3") +)] +#[derive( + Clone, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, + CustomResource, + Deserialize, + JsonSchema, + Serialize, +)] +#[serde(rename_all = "camelCase")] +struct PersonSpec { + username: String, + + // In v1alpha2 first and last name have been added + #[versioned(added(since = "v1alpha2"))] + first_name: String, + #[versioned(added(since = "v1alpha2"))] + last_name: String, + + // We started out with a enum. As we *need* to provide a default, we have a Unknown variant. + // Afterwards we figured let's be more flexible and accept any arbitrary String. + #[versioned( + added(since = "v2", default = "default_gender"), + changed(since = "v3", from_type = "Gender") + )] + gender: String, +} + +fn default_gender() -> Gender { + Gender::Unknown +} + +#[derive( + Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, JsonSchema, Serialize, +)] +#[serde(rename_all = "PascalCase")] +pub enum Gender { + Unknown, + Male, + Female, +} + +impl Into for Gender { + fn into(self) -> String { + match self { + Gender::Unknown => "Unknown".to_string(), + Gender::Male => "Male".to_string(), + Gender::Female => "Female".to_string(), + } + } +} +impl From for Gender { + fn from(value: String) -> Self { + match value.as_str() { + "Male" => Self::Male, + "Female" => Self::Female, + _ => Self::Unknown, + } + } +} + +// TEMP, we need to implement downgrades manually +impl From for v1alpha1::PersonSpec { + fn from(value: v1alpha2::PersonSpec) -> Self { + Self { + username: value.username, + } + } +} +impl From for v1alpha2::PersonSpec { + fn from(value: v1beta1::PersonSpec) -> Self { + Self { + username: value.username, + first_name: value.first_name, + last_name: value.last_name, + } + } +} +impl From for v1beta1::PersonSpec { + fn from(value: v2::PersonSpec) -> Self { + Self { + username: value.username, + first_name: value.first_name, + last_name: value.last_name, + } + } +} +impl From for v2::PersonSpec { + fn from(value: v3::PersonSpec) -> Self { + Self { + username: value.username, + first_name: value.first_name, + last_name: value.last_name, + gender: value.gender.into(), + } + } +} +// END TEMP + +#[cfg(test)] +mod tests { + use std::{fs::File, path::Path}; + + use insta::{assert_snapshot, glob}; + use kube::core::{conversion::ConversionReview, response::StatusSummary}; + + use super::Person; + + #[test] + fn pass() { + glob!("../../../fixtures/inputs/pass/", "*.json", |path| { + let (request, response) = run_for_file(path); + + let formatted = serde_json::to_string_pretty(&response) + .expect("Failed to serialize ConversionResponse"); + assert_snapshot!(formatted); + + let response = response + .response + .expect("ConversionReview had no response!"); + + assert_eq!( + response.result.status, + Some(StatusSummary::Success), + "File {path:?} should be converted successfully" + ); + assert_eq!(request.request.unwrap().uid, response.uid); + }) + } + + #[test] + fn fail() { + glob!("../../../fixtures/inputs/fail/", "*.json", |path| { + let (request, response) = run_for_file(path); + + let formatted = serde_json::to_string_pretty(&response) + .expect("Failed to serialize ConversionResponse"); + assert_snapshot!(formatted); + + let response = response + .response + .expect("ConversionReview had no response!"); + + assert_eq!( + response.result.status, + Some(StatusSummary::Failure), + "File {path:?} should *not* be converted successfully" + ); + if let Some(request) = &request.request { + assert_eq!(request.uid, response.uid); + } + }) + } + + fn run_for_file(path: &Path) -> (ConversionReview, ConversionReview) { + let request: ConversionReview = + serde_json::from_reader(File::open(path).expect("failed to open test file")) + .expect("failed to parse ConversionReview from test file"); + let response = Person::convert(request.clone()); + + (request, response) + } +} diff --git a/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__fail@request_missing.json.snap b/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__fail@request_missing.json.snap new file mode 100644 index 000000000..dd3931fc0 --- /dev/null +++ b/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__fail@request_missing.json.snap @@ -0,0 +1,19 @@ +--- +source: crates/stackable-versioned/src/flux_converter/tests/mod.rs +expression: formatted +input_file: crates/stackable-versioned/fixtures/inputs/fail/request_missing.json +--- +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "ConversionReview", + "response": { + "uid": "", + "result": { + "status": "Failure", + "code": 400, + "message": "The ConversionReview send did not include any request: request missing in ConversionReview", + "reason": "ConversionReview request missing" + }, + "convertedObjects": [] + } +} diff --git a/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__fail@undeserializable_missing_field.json.snap b/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__fail@undeserializable_missing_field.json.snap new file mode 100644 index 000000000..a713b1fa6 --- /dev/null +++ b/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__fail@undeserializable_missing_field.json.snap @@ -0,0 +1,19 @@ +--- +source: crates/stackable-versioned/src/flux_converter/tests/mod.rs +expression: formatted +input_file: crates/stackable-versioned/fixtures/inputs/fail/undeserializable_missing_field.json +--- +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "ConversionReview", + "response": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "result": { + "status": "Failure", + "code": 500, + "message": "failed to deserialize object of kind \"Person\": missing field `username`", + "reason": "failed to deserialize object of kind \"Person\": missing field `username`" + }, + "convertedObjects": [] + } +} diff --git a/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__fail@unkown_current_version.json.snap b/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__fail@unkown_current_version.json.snap new file mode 100644 index 000000000..4fdd3285e --- /dev/null +++ b/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__fail@unkown_current_version.json.snap @@ -0,0 +1,19 @@ +--- +source: crates/stackable-versioned/src/flux_converter/tests/mod.rs +expression: formatted +input_file: crates/stackable-versioned/fixtures/inputs/fail/unkown_current_version.json +--- +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "ConversionReview", + "response": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "result": { + "status": "Failure", + "code": 500, + "message": "failed to parse current resource version \"test.stackable.tech/v99\": the api version \"test.stackable.tech/v99\" is not known", + "reason": "failed to parse current resource version \"test.stackable.tech/v99\": the api version \"test.stackable.tech/v99\" is not known" + }, + "convertedObjects": [] + } +} diff --git a/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__fail@unkown_desired_version.json.snap b/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__fail@unkown_desired_version.json.snap new file mode 100644 index 000000000..4499203b6 --- /dev/null +++ b/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__fail@unkown_desired_version.json.snap @@ -0,0 +1,19 @@ +--- +source: crates/stackable-versioned/src/flux_converter/tests/mod.rs +expression: formatted +input_file: crates/stackable-versioned/fixtures/inputs/fail/unkown_desired_version.json +--- +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "ConversionReview", + "response": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "result": { + "status": "Failure", + "code": 500, + "message": "failed to parse desired resource version \"test.stackable.tech/v99\": the api version \"test.stackable.tech/v99\" is not known", + "reason": "failed to parse desired resource version \"test.stackable.tech/v99\": the api version \"test.stackable.tech/v99\" is not known" + }, + "convertedObjects": [] + } +} diff --git a/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__fail@wrong_object.json.snap b/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__fail@wrong_object.json.snap new file mode 100644 index 000000000..932eebbd8 --- /dev/null +++ b/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__fail@wrong_object.json.snap @@ -0,0 +1,19 @@ +--- +source: crates/stackable-versioned/src/flux_converter/tests/mod.rs +expression: formatted +input_file: crates/stackable-versioned/fixtures/inputs/fail/wrong_object.json +--- +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "ConversionReview", + "response": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "result": { + "status": "Failure", + "code": 400, + "message": "I was asked to convert the kind \"SomeOtherResource\", but I can only convert objects of kind \"Person\"", + "reason": "I was asked to convert the kind \"SomeOtherResource\", but I can only convert objects of kind \"Person\"" + }, + "convertedObjects": [] + } +} diff --git a/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__pass@persons_to_v1alpha1.json.snap b/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__pass@persons_to_v1alpha1.json.snap new file mode 100644 index 000000000..4ec591f94 --- /dev/null +++ b/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__pass@persons_to_v1alpha1.json.snap @@ -0,0 +1,57 @@ +--- +source: crates/stackable-versioned/src/flux_converter/tests/mod.rs +expression: formatted +input_file: crates/stackable-versioned/fixtures/inputs/pass/persons_to_v1alpha1.json +--- +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "ConversionReview", + "response": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "result": { + "status": "Success" + }, + "convertedObjects": [ + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": { + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "spec": { + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "spec": { + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "spec": { + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "spec": { + "username": "sbernauer" + } + } + ] + } +} diff --git a/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__pass@persons_to_v3.json.snap b/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__pass@persons_to_v3.json.snap new file mode 100644 index 000000000..f145a76ef --- /dev/null +++ b/crates/stackable-versioned/src/flux_converter/tests/snapshots/stackable_versioned__flux_converter__tests__tests__pass@persons_to_v3.json.snap @@ -0,0 +1,72 @@ +--- +source: crates/stackable-versioned/src/flux_converter/tests/mod.rs +expression: formatted +input_file: crates/stackable-versioned/fixtures/inputs/pass/persons_to_v3.json +--- +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "ConversionReview", + "response": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "result": { + "status": "Success" + }, + "convertedObjects": [ + { + "apiVersion": "test.stackable.tech/v3", + "kind": "Person", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": { + "firstName": "", + "gender": "Unknown", + "lastName": "", + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v3", + "kind": "Person", + "spec": { + "firstName": "Sebastian", + "gender": "Unknown", + "lastName": "Bernauer", + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v3", + "kind": "Person", + "spec": { + "firstName": "Sebastian", + "gender": "Unknown", + "lastName": "Bernauer", + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v3", + "kind": "Person", + "spec": { + "firstName": "Sebastian", + "gender": "Male", + "lastName": "Bernauer", + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v3", + "kind": "Person", + "spec": { + "firstName": "Sebastian", + "gender": "Male", + "lastName": "Bernauer", + "username": "sbernauer" + } + } + ] + } +} diff --git a/crates/stackable-versioned/src/lib.rs b/crates/stackable-versioned/src/lib.rs index 8c0c399b1..0499fd5f8 100644 --- a/crates/stackable-versioned/src/lib.rs +++ b/crates/stackable-versioned/src/lib.rs @@ -12,9 +12,27 @@ //! See [`versioned`] for an in-depth usage guide and a list of supported //! parameters. -// Re-export macro +use snafu::Snafu; pub use stackable_versioned_macros::*; +#[cfg(feature = "flux-converter")] +mod flux_converter; + +#[cfg(feature = "k8s")] +mod apply_resource; + +#[cfg(feature = "flux-converter")] +pub use flux_converter::ConversionError; + +#[derive(Debug, Snafu)] +pub enum ParseResourceVersionError { + #[snafu(display("the resource version \"{version}\" is not known"))] + UnknownResourceVersion { version: String }, + + #[snafu(display("the api version \"{api_version}\" is not known"))] + UnknownApiVersion { api_version: String }, +} + // Unused for now, might get picked up again in the future. #[doc(hidden)] pub trait AsVersionStr { diff --git a/crates/stackable-webhook/CHANGELOG.md b/crates/stackable-webhook/CHANGELOG.md index d3b39dca0..334112f77 100644 --- a/crates/stackable-webhook/CHANGELOG.md +++ b/crates/stackable-webhook/CHANGELOG.md @@ -28,8 +28,8 @@ All notable changes to this project will be documented in this file. ### Added -- Instrument `WebhookServer` with `AxumTraceLayer`, add static healthcheck without instrumentation ([#758]). -- Add shutdown signal hander for the `WebhookServer` ([#767]). +- Instrument `WebhookServer` with `AxumTraceLayer`, add static health-check without instrumentation ([#758]). +- Add shutdown signal handler for the `WebhookServer` ([#767]). ### Changed diff --git a/crates/stackable-webhook/src/constants.rs b/crates/stackable-webhook/src/constants.rs index 65f7c1ebb..1ba1e720c 100644 --- a/crates/stackable-webhook/src/constants.rs +++ b/crates/stackable-webhook/src/constants.rs @@ -8,5 +8,5 @@ pub const DEFAULT_HTTPS_PORT: u16 = 8443; /// The default IP address `127.0.0.1` the webhook server binds to. pub const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); -/// The default socket address `127.0.0.1:8443` the webhook server vinds to. +/// The default socket address `127.0.0.1:8443` the webhook server binds to. pub const DEFAULT_SOCKET_ADDR: SocketAddr = SocketAddr::new(DEFAULT_IP_ADDRESS, DEFAULT_HTTPS_PORT); diff --git a/crates/stackable-webhook/src/lib.rs b/crates/stackable-webhook/src/lib.rs index e1bb001a9..075a28da9 100644 --- a/crates/stackable-webhook/src/lib.rs +++ b/crates/stackable-webhook/src/lib.rs @@ -1,10 +1,10 @@ //! Utility types and functions to easily create ready-to-use webhook servers //! which can handle different tasks, for example CRD conversions. All webhook -//! servers use HTTPS by defaultThis library is fully compatible with the +//! servers use HTTPS by default. This library is fully compatible with the //! [`tracing`] crate and emits debug level tracing data. //! //! Most users will only use the top-level exported generic [`WebhookServer`] -//! which enables complete control over the [Router] which handles registering +//! which enables complete control over the [`Router`] which handles registering //! routes and their handler functions. //! //! ``` @@ -20,7 +20,7 @@ //! only required parameters are a conversion handler function and [`Options`]. //! //! This library additionally also exposes lower-level structs and functions to -//! enable complete controll over these details if needed. +//! enable complete control over these details if needed. //! //! [1]: crate::servers::ConversionWebhookServer use axum::{Router, routing::get}; diff --git a/crates/stackable-webhook/src/servers/conversion.rs b/crates/stackable-webhook/src/servers/conversion.rs index 922a9b431..ec29e8b75 100644 --- a/crates/stackable-webhook/src/servers/conversion.rs +++ b/crates/stackable-webhook/src/servers/conversion.rs @@ -39,9 +39,9 @@ pub struct ConversionWebhookServer { impl ConversionWebhookServer { /// Creates a new conversion webhook server **without** state which expects - /// POST requests being made to the `/convert` endpoint. + /// POST requests being made to the `/convert/{kind}` endpoints. /// - /// Each request is handled by the provided `handler` function. Any function + /// Each request is handled by the provided `handler` functions. Any function /// with the signature `(ConversionReview) -> ConversionReview` can be /// provided. The [`ConversionReview`] type can be imported via a re-export at /// [`crate::servers::ConversionReview`]. @@ -49,40 +49,44 @@ impl ConversionWebhookServer { /// # Example /// /// ``` + /// use stackable_operator::crd::authentication::core::AuthenticationClass; /// use stackable_webhook::{ /// servers::{ConversionReview, ConversionWebhookServer}, /// Options /// }; /// - /// // Construct the conversion webhook server - /// let server = ConversionWebhookServer::new(handler, Options::default()); + /// let handlers = [( + /// "AuthenticationClass", + /// AuthenticationClass::convert as fn(ConversionReview) -> ConversionReview, + /// )]; /// - /// // Define the handler function - /// fn handler(req: ConversionReview) -> ConversionReview { - /// // In here we can do the CRD conversion - /// req - /// } + /// // Construct the conversion webhook server + /// let server = ConversionWebhookServer::new(handlers, Options::default()); /// ``` - #[instrument(name = "create_conversion_webhhok_server", skip(handler))] - pub fn new(handler: H, options: Options) -> Self + #[instrument(name = "create_conversion_webhook_server", skip(handlers))] + pub fn new<'a, H>(handlers: impl IntoIterator, options: Options) -> Self where H: WebhookHandler + Clone + Send + Sync + 'static, { - tracing::debug!("create new conversion webhook server"); + tracing::debug!("creating new conversion webhook server"); + + let mut router = Router::new(); + for (kind, handler) in handlers { + let handler_fn = |Json(review): Json| async { + let review = handler.call(review); + Json(review) + }; - let handler_fn = |Json(review): Json| async { - let review = handler.call(review); - Json(review) - }; + router = router.route(&format!("/convert/{kind}"), post(handler_fn)); + } - let router = Router::new().route("/convert", post(handler_fn)); Self { router, options } } - /// Creates a new conversion webhook server **with** state which expects - /// POST requests being made to the `/convert` endpoint. + /// Creates a new conversion webhook server **without** state which expects + /// POST requests being made to the `/convert/{kind}` endpoints. /// - /// Each request is handled by the provided `handler` function. Any function + /// Each request is handled by the provided `handler` functions. Any function /// with the signature `(ConversionReview, S) -> ConversionReview` can be /// provided. The [`ConversionReview`] type can be imported via a re-export at /// [`crate::servers::ConversionReview`]. @@ -104,21 +108,30 @@ impl ConversionWebhookServer { /// #[derive(Debug, Clone)] /// struct State {} /// + /// let handlers = [( + /// "AuthenticationClass", + /// auth_class_handler as fn(ConversionReview, state: Arc) -> ConversionReview, + /// )]; + /// /// let shared_state = Arc::new(State {}); /// let server = ConversionWebhookServer::new_with_state( - /// handler, + /// handlers, /// shared_state, /// Options::default(), /// ); /// /// // Define the handler function - /// fn handler(req: ConversionReview, state: Arc) -> ConversionReview { + /// fn auth_class_handler(req: ConversionReview, state: Arc) -> ConversionReview { /// // In here we can do the CRD conversion /// req /// } /// ``` - #[instrument(name = "create_conversion_webhook_server_with_state", skip(handler))] - pub fn new_with_state(handler: H, state: S, options: Options) -> Self + #[instrument(name = "create_conversion_webhook_server_with_state", skip(handlers))] + pub fn new_with_state<'a, H, S>( + handlers: impl IntoIterator, + state: S, + options: Options, + ) -> Self where H: StatefulWebhookHandler + Clone @@ -127,23 +140,25 @@ impl ConversionWebhookServer { + 'static, S: Clone + Debug + Send + Sync + 'static, { - tracing::debug!("create new conversion webhook server with state"); + tracing::debug!("creating new conversion webhook server with state"); - // NOTE (@Techassi): Initially, after adding the state extractor, the - // compiler kept throwing a trait error at me stating that the closure - // below doesn't implement the Handler trait from Axum. This had nothing - // to do with the state itself, but rather the order of extractors. All - // body consuming extractors, like the JSON extractor need to come last - // in the handler. - // https://docs.rs/axum/latest/axum/extract/index.html#the-order-of-extractors - let handler_fn = |State(state): State, Json(review): Json| async { - let review = handler.call(review, state); - Json(review) - }; + let mut router = Router::new(); + for (kind, handler) in handlers { + // NOTE (@Techassi): Initially, after adding the state extractor, the + // compiler kept throwing a trait error at me stating that the closure + // below doesn't implement the Handler trait from Axum. This had nothing + // to do with the state itself, but rather the order of extractors. All + // body consuming extractors, like the JSON extractor need to come last + // in the handler. + // https://docs.rs/axum/latest/axum/extract/index.html#the-order-of-extractors + let handler_fn = |State(state): State, Json(review): Json| async { + let review = handler.call(review, state); + Json(review) + }; - let router = Router::new() - .route("/convert", post(handler_fn)) - .with_state(state); + router = router.route(&format!("/convert/{kind}"), post(handler_fn)); + } + let router = router.with_state(state); Self { router, options } } diff --git a/crates/stackable-webhook/src/tls.rs b/crates/stackable-webhook/src/tls.rs index f3bbef959..eeb0dbf46 100644 --- a/crates/stackable-webhook/src/tls.rs +++ b/crates/stackable-webhook/src/tls.rs @@ -8,7 +8,11 @@ use hyper::{body::Incoming, service::service_fn}; use hyper_util::rt::{TokioExecutor, TokioIo}; use opentelemetry::trace::{FutureExt, SpanKind}; use snafu::{ResultExt, Snafu}; -use stackable_certs::{CertificatePairError, ca::CertificateAuthority, keys::rsa}; +use stackable_certs::{ + CertificatePairError, + ca::{CertificateAuthority, DEFAULT_CA_VALIDITY_SECONDS}, + keys::ecdsa, +}; use stackable_operator::time::Duration; use tokio::net::TcpListener; use tokio_rustls::{ @@ -44,12 +48,12 @@ pub enum Error { #[snafu(display("failed to encode leaf certificate as DER"))] EncodeCertificateDer { - source: CertificatePairError, + source: CertificatePairError, }, #[snafu(display("failed to encode private key as DER"))] EncodePrivateKeyDer { - source: CertificatePairError, + source: CertificatePairError, }, #[snafu(display("failed to set safe TLS protocol versions"))] @@ -62,7 +66,7 @@ pub enum Error { /// Custom implementation of [`std::cmp::PartialEq`] because some inner types /// don't implement it. /// -/// Note that this implementation is restritced to testing because there are +/// Note that this implementation is restricted to testing because there are /// variants that use [`stackable_certs::ca::Error`] which only implements /// [`PartialEq`] for tests. #[cfg(test)] @@ -84,7 +88,7 @@ impl PartialEq for Error { } } -/// A server which terminates TLS connections and allows clients to commnunicate +/// A server which terminates TLS connections and allows clients to communicate /// via HTTPS with the underlying HTTP router. pub struct TlsServer { config: Arc, @@ -96,17 +100,20 @@ impl TlsServer { #[instrument(name = "create_tls_server", skip(router))] pub async fn new(socket_addr: SocketAddr, router: Router) -> Result { // NOTE(@NickLarsenNZ): This code is not async, and does take some - // non-negligable amount of time to complete (moreso in debug ). + // non-negligible amount of time to complete (moreso in debug). // We run this in a thread reserved for blocking code so that the Tokio // executor is able to make progress on other futures instead of being // blocked. // See https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html let task = tokio::task::spawn_blocking(move || { let mut certificate_authority = - CertificateAuthority::new_rsa().context(CreateCertificateAuthoritySnafu)?; - + CertificateAuthority::new_ecdsa().context(CreateCertificateAuthoritySnafu)?; let leaf_certificate = certificate_authority - .generate_rsa_leaf_certificate("Leaf", "webhook", Duration::from_secs(3600)) + .generate_ecdsa_leaf_certificate( + "Leaf", + "webhook", + Duration::from_secs(DEFAULT_CA_VALIDITY_SECONDS), + ) .context(GenerateLeafCertificateSnafu)?; let certificate_der = leaf_certificate