diff --git a/Cargo.lock b/Cargo.lock index a1f305135..5ba343419 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1653,8 +1653,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 +1665,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 +1701,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 +1719,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 +1732,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", @@ -3210,7 +3205,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]] @@ -3236,6 +3239,7 @@ dependencies = [ "snafu 0.8.5", "stackable-versioned", "syn 2.0.101", + "tracing", "trybuild", ] @@ -3263,6 +3267,18 @@ dependencies = [ "tracing-opentelemetry", ] +[[package]] +name = "stackable-webhook-example" +version = "0.0.1" +dependencies = [ + "snafu 0.8.5", + "stackable-operator", + "stackable-webhook", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "strsim" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index 4c1163a73..7e77a8477 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,3 +87,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 34918f52d..ff14587d2 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..bf5c6400d 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 @@ -112,6 +112,22 @@ impl ::std::fmt::Display for Foo { } } #[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(), + }) + } + } + } +} +#[automatically_derived] impl Foo { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( @@ -130,3 +146,321 @@ impl Foo { ) } } +#[automatically_derived] +impl Foo { + #[tracing::instrument(skip_all)] + 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) => { + tracing::debug!( + "Failed to converted objects of type {type}", type = stringify!(Foo), + ); + 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 = ::from_str( + &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 = ::from_str( + 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: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1Alpha1, Self::V1Beta1) => { + let resource: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1beta1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1beta1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1Alpha1, Self::V1) => { + let resource: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1beta1::FooSpec = resource.into(); + let resource: v1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1"), conversion + .steps = 2usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1Beta1, Self::V1Alpha1) => { + let resource: v1beta1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1alpha1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1beta1), to = stringify!("v1alpha1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1Beta1, Self::V1Beta1) => { + let resource: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1Beta1, Self::V1) => { + let resource: v1beta1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1beta1), to = stringify!("v1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1, Self::V1Alpha1) => { + let resource: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1beta1::FooSpec = resource.into(); + let resource: v1alpha1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1alpha1"), conversion + .steps = 2usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1, Self::V1Beta1) => { + let resource: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1beta1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1beta1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1, Self::V1) => { + let resource: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + } + } + 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..50919d889 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 @@ -115,6 +115,22 @@ impl ::std::fmt::Display for Foo { } } #[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(), + }) + } + } + } +} +#[automatically_derived] impl Foo { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( @@ -133,3 +149,321 @@ impl Foo { ) } } +#[automatically_derived] +impl Foo { + #[tracing::instrument(skip_all)] + 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) => { + tracing::debug!( + "Failed to converted objects of type {type}", type = stringify!(Foo), + ); + 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 = ::from_str( + &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 = ::from_str( + 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: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1Alpha1, Self::V1Beta1) => { + let resource: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1beta1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1beta1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1Alpha1, Self::V1) => { + let resource: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1beta1::FooSpec = resource.into(); + let resource: v1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1"), conversion + .steps = 2usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1Beta1, Self::V1Alpha1) => { + let resource: v1beta1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1alpha1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1beta1), to = stringify!("v1alpha1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1Beta1, Self::V1Beta1) => { + let resource: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1Beta1, Self::V1) => { + let resource: v1beta1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1beta1), to = stringify!("v1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1, Self::V1Alpha1) => { + let resource: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1beta1::FooSpec = resource.into(); + let resource: v1alpha1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1alpha1"), conversion + .steps = 2usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1, Self::V1Beta1) => { + let resource: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1beta1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1beta1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1, Self::V1) => { + let resource: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + } + } + 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..3e2e16023 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 @@ -229,6 +229,22 @@ impl ::std::fmt::Display for Foo { } } #[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(), + }) + } + } + } +} +#[automatically_derived] impl Foo { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( @@ -248,6 +264,324 @@ impl Foo { } } #[automatically_derived] +impl Foo { + #[tracing::instrument(skip_all)] + 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) => { + tracing::debug!( + "Failed to converted objects of type {type}", type = stringify!(Foo), + ); + 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 = ::from_str( + &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 = ::from_str( + 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: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1Alpha1, Self::V1) => { + let resource: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1Alpha1, Self::V2Alpha1) => { + let resource: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1::FooSpec = resource.into(); + let resource: v2alpha1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v2alpha1"), + conversion.steps = 2usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1, Self::V1Alpha1) => { + let resource: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1alpha1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1alpha1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1, Self::V1) => { + let resource: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1, Self::V2Alpha1) => { + let resource: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v2alpha1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v2alpha1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V2Alpha1, Self::V1Alpha1) => { + let resource: v2alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1::FooSpec = resource.into(); + let resource: v1alpha1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v1alpha1"), + conversion.steps = 2usize, + "Successfully converted {type} object", type = stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V2Alpha1, Self::V1) => { + let resource: v2alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V2Alpha1, Self::V2Alpha1) => { + let resource: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + } + } + Ok(converted) + } +} +#[automatically_derived] pub(crate) enum Bar { V1Alpha1, V1, @@ -267,6 +601,22 @@ impl ::std::fmt::Display for Bar { } } #[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(), + }) + } + } + } +} +#[automatically_derived] impl Bar { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( @@ -285,3 +635,321 @@ impl Bar { ) } } +#[automatically_derived] +impl Bar { + #[tracing::instrument(skip_all)] + 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) => { + tracing::debug!( + "Failed to converted objects of type {type}", type = stringify!(Bar), + ); + 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 = ::from_str( + &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 = ::from_str( + 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: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + (Self::V1Alpha1, Self::V1) => { + let resource: v1alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource: v1::BarSpec = resource.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Bar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + (Self::V1Alpha1, Self::V2Alpha1) => { + let resource: v1alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource: v1::BarSpec = resource.into(); + let resource: v2alpha1::BarSpec = resource.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v2alpha1"), + conversion.steps = 2usize, + "Successfully converted {type} object", type = stringify!(Bar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + (Self::V1, Self::V1Alpha1) => { + let resource: v1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource: v1alpha1::BarSpec = resource.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1alpha1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Bar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + (Self::V1, Self::V1) => { + let resource: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + (Self::V1, Self::V2Alpha1) => { + let resource: v1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource: v2alpha1::BarSpec = resource.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v2alpha1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Bar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + (Self::V2Alpha1, Self::V1Alpha1) => { + let resource: v2alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource: v1::BarSpec = resource.into(); + let resource: v1alpha1::BarSpec = resource.into(); + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v1alpha1"), + conversion.steps = 2usize, + "Successfully converted {type} object", type = stringify!(Bar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + (Self::V2Alpha1, Self::V1) => { + let resource: v2alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource: v1::BarSpec = resource.into(); + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(Bar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + (Self::V2Alpha1, Self::V2Alpha1) => { + let resource: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + } + } + 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..b63502241 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,21 @@ pub(crate) mod versioned { } } } + 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(), + }) + } + } + } + } impl Foo { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( @@ -235,6 +250,333 @@ pub(crate) mod versioned { ) } } + #[automatically_derived] + impl Foo { + #[tracing::instrument(skip_all)] + 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) => { + tracing::debug!( + "Failed to converted objects of type {type}", type = + stringify!(Foo), + ); + 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 = ::from_str( + &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 = ::from_str( + 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: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1Alpha1, Self::V1) => { + let resource: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1Alpha1, Self::V2Alpha1) => { + let resource: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1::FooSpec = resource.into(); + let resource: v2alpha1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v2alpha1"), + conversion.steps = 2usize, + "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1, Self::V1Alpha1) => { + let resource: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1alpha1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1alpha1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1, Self::V1) => { + let resource: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V1, Self::V2Alpha1) => { + let resource: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v2alpha1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v2alpha1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V2Alpha1, Self::V1Alpha1) => { + let resource: v2alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1::FooSpec = resource.into(); + let resource: v1alpha1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v1alpha1"), + conversion.steps = 2usize, + "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V2Alpha1, Self::V1) => { + let resource: v2alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?; + let resource: v1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(Foo), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + (Self::V2Alpha1, Self::V2Alpha1) => { + let resource: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + } + } + Ok(converted) + } + } pub enum Bar { V1Alpha1, V1, @@ -252,6 +594,21 @@ pub(crate) mod versioned { } } } + 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(), + }) + } + } + } + } impl Bar { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( @@ -270,4 +627,331 @@ pub(crate) mod versioned { ) } } + #[automatically_derived] + impl Bar { + #[tracing::instrument(skip_all)] + 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) => { + tracing::debug!( + "Failed to converted objects of type {type}", type = + stringify!(Bar), + ); + 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 = ::from_str( + &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 = ::from_str( + 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: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + (Self::V1Alpha1, Self::V1) => { + let resource: v1alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource: v1::BarSpec = resource.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(Bar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + (Self::V1Alpha1, Self::V2Alpha1) => { + let resource: v1alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource: v1::BarSpec = resource.into(); + let resource: v2alpha1::BarSpec = resource.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v2alpha1"), + conversion.steps = 2usize, + "Successfully converted {type} object", type = + stringify!(Bar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + (Self::V1, Self::V1Alpha1) => { + let resource: v1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource: v1alpha1::BarSpec = resource.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1alpha1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(Bar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + (Self::V1, Self::V1) => { + let resource: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + (Self::V1, Self::V2Alpha1) => { + let resource: v1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource: v2alpha1::BarSpec = resource.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v2alpha1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(Bar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + (Self::V2Alpha1, Self::V1Alpha1) => { + let resource: v2alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource: v1::BarSpec = resource.into(); + let resource: v1alpha1::BarSpec = resource.into(); + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v1alpha1"), + conversion.steps = 2usize, + "Successfully converted {type} object", type = + stringify!(Bar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + (Self::V2Alpha1, Self::V1) => { + let resource: v2alpha1::BarSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?; + let resource: v1::BarSpec = resource.into(); + tracing::trace!( + from = stringify!(v2alpha1), to = stringify!("v1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(Bar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + (Self::V2Alpha1, Self::V2Alpha1) => { + let resource: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Bar).to_string(), + })?, + ); + } + } + } + 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..75dbfd46f 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 @@ -112,6 +112,22 @@ impl ::std::fmt::Display for FooBar { } } #[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(), + }) + } + } + } +} +#[automatically_derived] impl FooBar { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( @@ -130,3 +146,326 @@ impl FooBar { ) } } +#[automatically_derived] +impl FooBar { + #[tracing::instrument(skip_all)] + 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) => { + tracing::debug!( + "Failed to converted objects of type {type}", type = + stringify!(FooBar), + ); + 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 = ::from_str( + &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 = ::from_str( + 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: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?, + ); + } + (Self::V1Alpha1, Self::V1Beta1) => { + let resource: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + let resource: v1beta1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1beta1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(FooBar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?, + ); + } + (Self::V1Alpha1, Self::V1) => { + let resource: v1alpha1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + let resource: v1beta1::FooSpec = resource.into(); + let resource: v1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1alpha1), to = stringify!("v1"), conversion + .steps = 2usize, "Successfully converted {type} object", type = + stringify!(FooBar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?, + ); + } + (Self::V1Beta1, Self::V1Alpha1) => { + let resource: v1beta1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + let resource: v1alpha1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1beta1), to = stringify!("v1alpha1"), + conversion.steps = 1usize, + "Successfully converted {type} object", type = + stringify!(FooBar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?, + ); + } + (Self::V1Beta1, Self::V1Beta1) => { + let resource: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?, + ); + } + (Self::V1Beta1, Self::V1) => { + let resource: v1beta1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + let resource: v1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1beta1), to = stringify!("v1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(FooBar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?, + ); + } + (Self::V1, Self::V1Alpha1) => { + let resource: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + let resource: v1beta1::FooSpec = resource.into(); + let resource: v1alpha1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1alpha1"), conversion + .steps = 2usize, "Successfully converted {type} object", type = + stringify!(FooBar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?, + ); + } + (Self::V1, Self::V1Beta1) => { + let resource: v1::FooSpec = serde_json::from_value( + object_spec.clone(), + ) + .map_err(|err| ConversionError::DeserializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?; + let resource: v1beta1::FooSpec = resource.into(); + tracing::trace!( + from = stringify!(v1), to = stringify!("v1beta1"), conversion + .steps = 1usize, "Successfully converted {type} object", type = + stringify!(FooBar), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?, + ); + } + (Self::V1, Self::V1) => { + let resource: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(FooBar).to_string(), + })?, + ); + } + } + } + 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..17ca22651 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 @@ -39,6 +39,20 @@ impl ::std::fmt::Display for Foo { } } #[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(), + }) + } + } + } +} +#[automatically_derived] impl Foo { /// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored. pub fn merged_crd( @@ -53,3 +67,137 @@ impl Foo { ) } } +#[automatically_derived] +impl Foo { + #[tracing::instrument(skip_all)] + 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) => { + tracing::debug!( + "Failed to converted objects of type {type}", type = stringify!(Foo), + ); + 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 = ::from_str( + &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 = ::from_str( + 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: 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), + ); + converted + .push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec { + source: err, + kind: stringify!(Foo).to_string(), + })?, + ); + } + } + } + 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..79d6b1716 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,114 @@ pub mod v1 { pub baz: bool, } } +#[automatically_derived] +impl Foo { + #[tracing::instrument(skip_all)] + 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) => { + tracing::debug!( + "Failed to converted objects of type {type}", type = stringify!(Foo), + ); + 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 = ::from_str( + &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 = ::from_str( + 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..c29afc1dc 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,33 @@ 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, + )); + + Some(tokens) } pub(crate) fn get_original_ident(&self) -> &Ident { @@ -214,7 +231,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..012a2634f 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct.rs @@ -349,6 +349,8 @@ 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 enum_ident = &self.common.idents.kubernetes; @@ -377,12 +379,27 @@ impl Struct { } } + #automatically_derived + impl ::std::str::FromStr for #enum_ident { + type Err = stackable_versioned::ParseResourceVersionError; + + fn from_str(version: &str) -> Result { + match version { + #(#enum_variant_strings => Ok(Self::#enum_variant_idents),)* + _ => Err(stackable_versioned::ParseResourceVersionError::UnknownResourceVersion{version: 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..2bac6ec80 --- /dev/null +++ b/crates/stackable-versioned-macros/src/codegen/flux_converter.rs @@ -0,0 +1,230 @@ +use darling::util::IdentString; +use proc_macro2::TokenStream; +use quote::quote; + +pub(crate) fn generate_kubernetes_conversion( + enum_ident: &IdentString, + struct_ident: &IdentString, + enum_variant_idents: &[IdentString], + enum_variant_strings: &[String], +) -> Option { + assert_eq!(enum_variant_idents.len(), enum_variant_strings.len()); + + 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: #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: #version_chain_string::#struct_ident = resource.into(); + )* + + tracing::trace!( + from = stringify!(#src_lower), + to = stringify!(#dst_lower), + conversion.steps = #steps, + "Successfully converted {type} object", + type = stringify!(#enum_ident), + ); + + converted.push( + serde_json::to_value(resource) + .map_err(|err| ConversionError::SerializeObjectSpec{source: err, kind: stringify!(#enum_ident).to_string()})? + ); + }} + }, + ); + + 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::conversion::ConversionReview) -> kube::core::conversion::ConversionReview { + // Intentionally not using `snafu::ResultExt` here to keep the number of dependencies minimal + 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!(#enum_ident), + ); + + conversion_response.success(converted).into_review() + }, + Err(err) => { + tracing::debug!( + "Failed to converted objects of type {type}", + type = stringify!(#enum_ident), + ); + + 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; + + // FIXME: Check that request.types.{kind,api_version} match the expected values + + let desired_object_version = ::from_str(&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 = ::from_str(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..33bca0d9b --- /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": "v3", + "objects": [ + { + "apiVersion": "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..5b7c29e80 --- /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": "v3", + "objects": [ + { + "apiVersion": "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..17d903f43 --- /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": "v99", + "objects": [ + { + "apiVersion": "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..0e8b24f2e --- /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": "v3", + "objects": [ + { + "apiVersion": "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..c27a60f4b --- /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": "v1alpha1", + "objects": [ + { + "apiVersion": "v1alpha1", + "kind": "Person", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": { + "username": "sbernauer" + } + }, + { + "apiVersion": "v1alpha2", + "kind": "Person", + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer" + } + }, + { + "apiVersion": "v1beta1", + "kind": "Person", + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer" + } + }, + { + "apiVersion": "v2", + "kind": "Person", + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer", + "gender": "Male" + } + }, + { + "apiVersion": "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..fa678172f --- /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": "v3", + "objects": [ + { + "apiVersion": "v1alpha1", + "kind": "Person", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": { + "username": "sbernauer" + } + }, + { + "apiVersion": "v1alpha2", + "kind": "Person", + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer" + } + }, + { + "apiVersion": "v1beta1", + "kind": "Person", + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer" + } + }, + { + "apiVersion": "v2", + "kind": "Person", + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer", + "gender": "Male" + } + }, + { + "apiVersion": "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..3aaad92fe --- /dev/null +++ b/crates/stackable-versioned/src/flux_converter/tests/mod.rs @@ -0,0 +1,180 @@ +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 response = response + .response + .expect("ConversionReview had no response!"); + + let formatted = serde_json::to_string_pretty(&response) + .expect("Failed to serialize ConversionResponse"); + assert_snapshot!(formatted); + + 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 response = response + .response + .expect("ConversionReview had no response!"); + + let formatted = serde_json::to_string_pretty(&response) + .expect("Failed to serialize ConversionResponse"); + assert_snapshot!(formatted); + + 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..cfed7020d --- /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,15 @@ +--- +source: crates/stackable-versioned/src/flux_converter/tests/mod.rs +expression: formatted +input_file: crates/stackable-versioned/fixtures/inputs/fail/request_missing.json +--- +{ + "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..2079f0cb4 --- /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,15 @@ +--- +source: crates/stackable-versioned/src/flux_converter/tests/mod.rs +expression: formatted +input_file: crates/stackable-versioned/fixtures/inputs/fail/undeserializable_missing_field.json +--- +{ + "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..98bf3008a --- /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,15 @@ +--- +source: crates/stackable-versioned/src/flux_converter/tests/mod.rs +expression: formatted +input_file: crates/stackable-versioned/fixtures/inputs/fail/unkown_current_version.json +--- +{ + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "result": { + "status": "Failure", + "code": 500, + "message": "failed to parse current resource version \"v99\": the resource version \"v99\" is not known", + "reason": "failed to parse current resource version \"v99\": the resource version \"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..f3ebe1f7f --- /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,15 @@ +--- +source: crates/stackable-versioned/src/flux_converter/tests/mod.rs +expression: formatted +input_file: crates/stackable-versioned/fixtures/inputs/fail/unkown_desired_version.json +--- +{ + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "result": { + "status": "Failure", + "code": 500, + "message": "failed to parse desired resource version \"v99\": the resource version \"v99\" is not known", + "reason": "failed to parse desired resource version \"v99\": the resource version \"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..e4c3f6617 --- /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,15 @@ +--- +source: crates/stackable-versioned/src/flux_converter/tests/mod.rs +expression: formatted +input_file: crates/stackable-versioned/fixtures/inputs/fail/wrong_object.json +--- +{ + "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..98a2e8ba0 --- /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,28 @@ +--- +source: crates/stackable-versioned/src/flux_converter/tests/mod.rs +expression: formatted +input_file: crates/stackable-versioned/fixtures/inputs/pass/persons_to_v1alpha1.json +--- +{ + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "result": { + "status": "Success" + }, + "convertedObjects": [ + { + "username": "sbernauer" + }, + { + "username": "sbernauer" + }, + { + "username": "sbernauer" + }, + { + "username": "sbernauer" + }, + { + "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..5a8bfccd3 --- /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,43 @@ +--- +source: crates/stackable-versioned/src/flux_converter/tests/mod.rs +expression: formatted +input_file: crates/stackable-versioned/fixtures/inputs/pass/persons_to_v3.json +--- +{ + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "result": { + "status": "Success" + }, + "convertedObjects": [ + { + "firstName": "", + "gender": "Unknown", + "lastName": "", + "username": "sbernauer" + }, + { + "firstName": "Sebastian", + "gender": "Unknown", + "lastName": "Bernauer", + "username": "sbernauer" + }, + { + "firstName": "Sebastian", + "gender": "Unknown", + "lastName": "Bernauer", + "username": "sbernauer" + }, + { + "firstName": "Sebastian", + "gender": "Male", + "lastName": "Bernauer", + "username": "sbernauer" + }, + { + "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..bb2df6ae3 100644 --- a/crates/stackable-versioned/src/lib.rs +++ b/crates/stackable-versioned/src/lib.rs @@ -12,9 +12,24 @@ //! 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 }, +} + // 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