diff --git a/Cargo.lock b/Cargo.lock index fc857bfd..c89c0e3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3067,7 +3067,10 @@ dependencies = [ name = "stackable-versioned" version = "0.7.1" dependencies = [ + "insta", + "k8s-openapi", "k8s-version", + "kube", "schemars", "serde", "serde_json", diff --git a/crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs b/crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs index 53b3b536..c9fa59e3 100644 --- a/crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs +++ b/crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs @@ -129,6 +129,7 @@ impl Struct { let variant_data_ident = &self.common.idents.kubernetes_parameter; let version_enum_ident = &self.common.idents.kubernetes_version; let enum_ident = &self.common.idents.kubernetes; + let enum_ident_string = enum_ident.to_string(); // Only add the #[automatically_derived] attribute if this impl is used outside of a // module (in standalone mode). @@ -153,7 +154,8 @@ impl Struct { // Generate additional Kubernetes code, this is split out to reduce the complexity in this // function. let status_struct = self.generate_kubernetes_status_struct(kubernetes_arguments, is_nested); - let version_enum = self.generate_kubernetes_version_enum(tokens, vis, is_nested); + let version_enum = + self.generate_kubernetes_version_enum(kubernetes_arguments, tokens, vis, is_nested); let convert_method = self.generate_kubernetes_conversion(versions); let parse_object_error = quote! { #versioned_path::ParseObjectError }; @@ -173,21 +175,39 @@ impl Struct { #k8s_openapi_path::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, #kube_core_path::crd::MergeError> { - #kube_core_path::crd::merge_crds(vec![#(#crd_fns),*], stored_apiversion.as_str()) + #kube_core_path::crd::merge_crds(vec![#(#crd_fns),*], stored_apiversion.as_version_str()) } #convert_method - fn from_json_value(value: #serde_json_path::Value) -> ::std::result::Result { - let api_version = value + fn from_json_object(object_value: #serde_json_path::Value) -> ::std::result::Result { + let object_kind = object_value + .get("kind") + .ok_or_else(|| #parse_object_error::FieldMissing{ field: "kind".to_owned() })? + .as_str() + .ok_or_else(|| #parse_object_error::FieldNotStr{ field: "kind".to_owned() })?; + + // Note(@sbernauer): The kind must be checked here, because it is + // possible for the wrong object to be deserialized. + // Checking here stops us assuming the kind is correct and + // accidentally updating upgrade/downgrade information in the + // status in a later step. + if object_kind != #enum_ident_string { + return Err(#parse_object_error::UnexpectedKind{ + kind: object_kind.to_owned(), + expected: #enum_ident_string.to_owned(), + }); + } + + let api_version = object_value .get("apiVersion") - .ok_or_else(|| #parse_object_error::FieldNotPresent)? + .ok_or_else(|| #parse_object_error::FieldMissing{ field: "apiVersion".to_owned() })? .as_str() - .ok_or_else(|| #parse_object_error::FieldNotStr)?; + .ok_or_else(|| #parse_object_error::FieldNotStr{ field: "apiVersion".to_owned() })?; let object = match api_version { - #(#api_versions | #variant_strings => { - let object = #serde_json_path::from_value(value) + #(#api_versions => { + let object = #serde_json_path::from_value(object_value) .map_err(|source| #parse_object_error::Deserialize { source })?; Self::#variant_idents(object) @@ -218,6 +238,7 @@ impl Struct { fn generate_kubernetes_version_enum( &self, + kubernetes_arguments: &KubernetesArguments, tokens: &KubernetesTokens, vis: &Visibility, is_nested: bool, @@ -228,12 +249,21 @@ impl Struct { // module (in standalone mode). let automatically_derived = is_nested.not().then(|| quote! {#[automatically_derived]}); + let versioned_path = &*kubernetes_arguments.crates.versioned; + let unknown_desired_api_version_error = + quote! { #versioned_path::UnknownDesiredApiVersionError }; + // Get the per-version items to be able to iterate over them via quote let variant_strings = &tokens.variant_strings; let variant_idents = &tokens.variant_idents; + let api_versions = variant_strings + .iter() + .map(|version| format!("{group}/{version}", group = &kubernetes_arguments.group)) + .collect::>(); quote! { #automatically_derived + #[derive(Copy, Clone, Debug)] #vis enum #enum_ident { #(#variant_idents),* } @@ -241,17 +271,33 @@ impl Struct { #automatically_derived impl ::std::fmt::Display for #enum_ident { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> { - f.write_str(self.as_str()) + // The version (without the Kubernetes group) is probably more human-readable + f.write_str(self.as_version_str()) } } #automatically_derived impl #enum_ident { - pub fn as_str(&self) -> &str { + pub fn as_version_str(&self) -> &str { match self { #(#variant_idents => #variant_strings),* } } + + pub fn as_api_version_str(&self) -> &str { + match self { + #(#variant_idents => #api_versions),* + } + } + + pub fn from_api_version(api_version: &str) -> Result { + match api_version { + #(#api_versions => Ok(Self::#variant_idents)),*, + _ => Err(#unknown_desired_api_version_error { + api_version: api_version.to_owned(), + }), + } + } } } } @@ -315,6 +361,7 @@ impl Struct { let kubernetes_arguments = self.common.options.kubernetes_arguments.as_ref()?; let struct_ident = &self.common.idents.kubernetes; + let version_enum_ident = &self.common.idents.kubernetes_version; let kube_client_path = &*kubernetes_arguments.crates.kube_client; let serde_json_path = &*kubernetes_arguments.crates.serde_json; @@ -324,7 +371,7 @@ impl Struct { let convert_object_error = quote! { #versioned_path::ConvertObjectError }; // Generate conversion paths and the match arms for these paths - let match_arms = + let conversion_match_arms = self.generate_kubernetes_conversion_match_arms(versions, kubernetes_arguments); // TODO (@Techassi): Make this a feature, drop the option from the macro arguments @@ -375,11 +422,8 @@ impl Struct { } }; - // Extract the desired api version - let desired_api_version = request.desired_api_version.as_str(); - // Convert all objects into the desired version - let response = match Self::convert_objects(request.objects, desired_api_version) { + let response = match Self::convert_objects(request.objects, &request.desired_api_version) { ::std::result::Result::Ok(converted_objects) => { #successful_conversion_response_event @@ -426,20 +470,31 @@ impl Struct { ) -> ::std::result::Result<::std::vec::Vec<#serde_json_path::Value>, #convert_object_error> { + let desired_api_version = #version_enum_ident::from_api_version(desired_api_version) + .map_err(|source| #convert_object_error::ParseDesiredApiVersion { source })?; + let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); for object in objects { // This clone is required because in the noop case we move the object into // the converted objects vec. - let current_object = Self::from_json_value(object.clone()) + let current_object = Self::from_json_object(object.clone()) .map_err(|source| #convert_object_error::Parse { source })?; match (current_object, desired_api_version) { - #(#match_arms,)* - // If no match arm matches, this is a noop. This is the case if the desired - // version matches the current object api version. + #(#conversion_match_arms,)* + // In case the desired version matches the current object api version, we + // don't need to do anything. + // // NOTE (@Techassi): I'm curious if this will ever happen? In theory the K8s // apiserver should never send such a conversion review. + // + // Note(@sbernauer): I would prefer to explicitly list the remaining no-op + // cases, so the compiler ensures we did not miss a conversion + // // let version_idents = versions.iter().map(|v| &v.idents.variant); + // #( + // (Self::#version_idents(_), #version_enum_ident::#version_idents) + // )|* => converted_objects.push(object) _ => converted_objects.push(object), } } @@ -454,8 +509,10 @@ impl Struct { versions: &[VersionDefinition], kubernetes_arguments: &KubernetesArguments, ) -> Vec { + let group = &kubernetes_arguments.group; let variant_data_ident = &self.common.idents.kubernetes_parameter; let struct_ident = &self.common.idents.kubernetes; + let version_enum_ident = &self.common.idents.kubernetes_version; let spec_ident = &self.common.idents.original; let versioned_path = &*kubernetes_arguments.crates.versioned; @@ -470,7 +527,10 @@ impl Struct { let current_object_version_string = &start.inner.to_string(); let desired_object_version = path.last().expect("the path always contains at least one element"); - let desired_object_version_string = desired_object_version.inner.to_string(); + let desired_object_api_version_string = format!( + "{group}/{desired_object_version}", + desired_object_version = desired_object_version.inner + ); let desired_object_variant_ident = &desired_object_version.idents.variant; let desired_object_module_ident = &desired_object_version.idents.module; @@ -493,7 +553,7 @@ impl Struct { let convert_object_trace = kubernetes_arguments.options.enable_tracing.is_present().then(|| quote! { ::tracing::trace!( - #DESIRED_API_VERSION_ATTRIBUTE = #desired_object_version_string, + #DESIRED_API_VERSION_ATTRIBUTE = #desired_object_api_version_string, #API_VERSION_ATTRIBUTE = #current_object_version_string, #STEPS_ATTRIBUTE = #steps, #KIND_ATTRIBUTE = #kind, @@ -507,7 +567,7 @@ impl Struct { .then(|| quote! { status: #variant_data_ident.status, }); quote! { - (Self::#current_object_version_ident(#variant_data_ident), #desired_object_version_string) => { + (Self::#current_object_version_ident(#variant_data_ident), #version_enum_ident::#desired_object_variant_ident) => { #(#conversions)* let desired_object = Self::#desired_object_variant_ident(#desired_object_module_ident::#struct_ident { diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@basic.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@basic.rs.snap index 24b00a58..fc3a3f40 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@basic.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@basic.rs.snap @@ -133,7 +133,7 @@ impl Foo { v1beta1::Foo as ::kube::core::CustomResourceExt > ::crd(), < v1::Foo as ::kube::core::CustomResourceExt > ::crd() ], - stored_apiversion.as_str(), + stored_apiversion.as_version_str(), ) } ///Tries to convert a list of objects of kind [`Foo`] to the desired API version @@ -161,10 +161,9 @@ impl Foo { .into_review(); } }; - let desired_api_version = request.desired_api_version.as_str(); let response = match Self::convert_objects( request.objects, - desired_api_version, + &request.desired_api_version, ) { ::std::result::Result::Ok(converted_objects) => { ::kube::core::conversion::ConversionResponse { @@ -200,14 +199,18 @@ impl Foo { ::std::vec::Vec<::serde_json::Value>, ::stackable_versioned::ConvertObjectError, > { + let desired_api_version = FooVersion::from_api_version(desired_api_version) + .map_err(|source| ::stackable_versioned::ConvertObjectError::ParseDesiredApiVersion { + source, + })?; let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); for object in objects { - let current_object = Self::from_json_value(object.clone()) + let current_object = Self::from_json_object(object.clone()) .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { source, })?; match (current_object, desired_api_version) { - (Self::V1Alpha1(__sv_foo), "v1beta1") => { + (Self::V1Alpha1(__sv_foo), FooVersion::V1Beta1) => { let converted: v1beta1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1Beta1(v1beta1::Foo { metadata: __sv_foo.metadata, @@ -221,7 +224,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1Alpha1(__sv_foo), "v1") => { + (Self::V1Alpha1(__sv_foo), FooVersion::V1) => { let converted: v1beta1::FooSpec = __sv_foo.spec.into(); let converted: v1::FooSpec = converted.into(); let desired_object = Self::V1(v1::Foo { @@ -236,7 +239,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1Beta1(__sv_foo), "v1alpha1") => { + (Self::V1Beta1(__sv_foo), FooVersion::V1Alpha1) => { let converted: v1alpha1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1Alpha1(v1alpha1::Foo { metadata: __sv_foo.metadata, @@ -250,7 +253,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1Beta1(__sv_foo), "v1") => { + (Self::V1Beta1(__sv_foo), FooVersion::V1) => { let converted: v1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1(v1::Foo { metadata: __sv_foo.metadata, @@ -264,7 +267,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1(__sv_foo), "v1alpha1") => { + (Self::V1(__sv_foo), FooVersion::V1Alpha1) => { let converted: v1beta1::FooSpec = __sv_foo.spec.into(); let converted: v1alpha1::FooSpec = converted.into(); let desired_object = Self::V1Alpha1(v1alpha1::Foo { @@ -279,7 +282,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1(__sv_foo), "v1beta1") => { + (Self::V1(__sv_foo), FooVersion::V1Beta1) => { let converted: v1beta1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1Beta1(v1beta1::Foo { metadata: __sv_foo.metadata, @@ -298,31 +301,50 @@ impl Foo { } ::std::result::Result::Ok(converted_objects) } - fn from_json_value( - value: ::serde_json::Value, + fn from_json_object( + object_value: ::serde_json::Value, ) -> ::std::result::Result { - let api_version = value + let object_kind = object_value + .get("kind") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "kind".to_owned(), + })? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "kind".to_owned(), + })?; + if object_kind != "Foo" { + return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { + kind: object_kind.to_owned(), + expected: "Foo".to_owned(), + }); + } + let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "apiVersion".to_owned(), + })? .as_str() - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "apiVersion".to_owned(), + })?; let object = match api_version { - "stackable.tech/v1alpha1" | "v1alpha1" => { - let object = ::serde_json::from_value(value) + "stackable.tech/v1alpha1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; Self::V1Alpha1(object) } - "stackable.tech/v1beta1" | "v1beta1" => { - let object = ::serde_json::from_value(value) + "stackable.tech/v1beta1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; Self::V1Beta1(object) } - "stackable.tech/v1" | "v1" => { - let object = ::serde_json::from_value(value) + "stackable.tech/v1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; @@ -347,6 +369,7 @@ impl Foo { } } #[automatically_derived] +#[derive(Copy, Clone, Debug)] pub(crate) enum FooVersion { V1Alpha1, V1Beta1, @@ -358,16 +381,37 @@ impl ::std::fmt::Display for FooVersion { &self, f: &mut ::std::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { - f.write_str(self.as_str()) + f.write_str(self.as_version_str()) } } #[automatically_derived] impl FooVersion { - pub fn as_str(&self) -> &str { + pub fn as_version_str(&self) -> &str { match self { V1Alpha1 => "v1alpha1", V1Beta1 => "v1beta1", V1 => "v1", } } + pub fn as_api_version_str(&self) -> &str { + match self { + V1Alpha1 => "stackable.tech/v1alpha1", + V1Beta1 => "stackable.tech/v1beta1", + V1 => "stackable.tech/v1", + } + } + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "stackable.tech/v1alpha1" => Ok(Self::V1Alpha1), + "stackable.tech/v1beta1" => Ok(Self::V1Beta1), + "stackable.tech/v1" => Ok(Self::V1), + _ => { + Err(::stackable_versioned::UnknownDesiredApiVersionError { + api_version: api_version.to_owned(), + }) + } + } + } } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@conversion_tracking.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@conversion_tracking.rs.snap index 9b72f118..dc096997 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@conversion_tracking.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@conversion_tracking.rs.snap @@ -103,7 +103,7 @@ impl Foo { v1beta1::Foo as ::kube::core::CustomResourceExt > ::crd(), < v1::Foo as ::kube::core::CustomResourceExt > ::crd() ], - stored_apiversion.as_str(), + stored_apiversion.as_version_str(), ) } ///Tries to convert a list of objects of kind [`Foo`] to the desired API version @@ -131,10 +131,9 @@ impl Foo { .into_review(); } }; - let desired_api_version = request.desired_api_version.as_str(); let response = match Self::convert_objects( request.objects, - desired_api_version, + &request.desired_api_version, ) { ::std::result::Result::Ok(converted_objects) => { ::kube::core::conversion::ConversionResponse { @@ -170,14 +169,18 @@ impl Foo { ::std::vec::Vec<::serde_json::Value>, ::stackable_versioned::ConvertObjectError, > { + let desired_api_version = FooVersion::from_api_version(desired_api_version) + .map_err(|source| ::stackable_versioned::ConvertObjectError::ParseDesiredApiVersion { + source, + })?; let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); for object in objects { - let current_object = Self::from_json_value(object.clone()) + let current_object = Self::from_json_object(object.clone()) .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { source, })?; match (current_object, desired_api_version) { - (Self::V1Alpha1(__sv_foo), "v1beta1") => { + (Self::V1Alpha1(__sv_foo), FooVersion::V1Beta1) => { let converted: v1beta1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1Beta1(v1beta1::Foo { metadata: __sv_foo.metadata, @@ -191,7 +194,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1Alpha1(__sv_foo), "v1") => { + (Self::V1Alpha1(__sv_foo), FooVersion::V1) => { let converted: v1beta1::FooSpec = __sv_foo.spec.into(); let converted: v1::FooSpec = converted.into(); let desired_object = Self::V1(v1::Foo { @@ -206,7 +209,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1Beta1(__sv_foo), "v1alpha1") => { + (Self::V1Beta1(__sv_foo), FooVersion::V1Alpha1) => { let converted: v1alpha1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1Alpha1(v1alpha1::Foo { metadata: __sv_foo.metadata, @@ -220,7 +223,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1Beta1(__sv_foo), "v1") => { + (Self::V1Beta1(__sv_foo), FooVersion::V1) => { let converted: v1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1(v1::Foo { metadata: __sv_foo.metadata, @@ -234,7 +237,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1(__sv_foo), "v1alpha1") => { + (Self::V1(__sv_foo), FooVersion::V1Alpha1) => { let converted: v1beta1::FooSpec = __sv_foo.spec.into(); let converted: v1alpha1::FooSpec = converted.into(); let desired_object = Self::V1Alpha1(v1alpha1::Foo { @@ -249,7 +252,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1(__sv_foo), "v1beta1") => { + (Self::V1(__sv_foo), FooVersion::V1Beta1) => { let converted: v1beta1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1Beta1(v1beta1::Foo { metadata: __sv_foo.metadata, @@ -268,31 +271,50 @@ impl Foo { } ::std::result::Result::Ok(converted_objects) } - fn from_json_value( - value: ::serde_json::Value, + fn from_json_object( + object_value: ::serde_json::Value, ) -> ::std::result::Result { - let api_version = value + let object_kind = object_value + .get("kind") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "kind".to_owned(), + })? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "kind".to_owned(), + })?; + if object_kind != "Foo" { + return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { + kind: object_kind.to_owned(), + expected: "Foo".to_owned(), + }); + } + let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "apiVersion".to_owned(), + })? .as_str() - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "apiVersion".to_owned(), + })?; let object = match api_version { - "stackable.tech/v1alpha1" | "v1alpha1" => { - let object = ::serde_json::from_value(value) + "stackable.tech/v1alpha1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; Self::V1Alpha1(object) } - "stackable.tech/v1beta1" | "v1beta1" => { - let object = ::serde_json::from_value(value) + "stackable.tech/v1beta1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; Self::V1Beta1(object) } - "stackable.tech/v1" | "v1" => { - let object = ::serde_json::from_value(value) + "stackable.tech/v1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; @@ -317,6 +339,7 @@ impl Foo { } } #[automatically_derived] +#[derive(Copy, Clone, Debug)] pub(crate) enum FooVersion { V1Alpha1, V1Beta1, @@ -328,18 +351,39 @@ impl ::std::fmt::Display for FooVersion { &self, f: &mut ::std::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { - f.write_str(self.as_str()) + f.write_str(self.as_version_str()) } } #[automatically_derived] impl FooVersion { - pub fn as_str(&self) -> &str { + pub fn as_version_str(&self) -> &str { match self { V1Alpha1 => "v1alpha1", V1Beta1 => "v1beta1", V1 => "v1", } } + pub fn as_api_version_str(&self) -> &str { + match self { + V1Alpha1 => "stackable.tech/v1alpha1", + V1Beta1 => "stackable.tech/v1beta1", + V1 => "stackable.tech/v1", + } + } + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "stackable.tech/v1alpha1" => Ok(Self::V1Alpha1), + "stackable.tech/v1beta1" => Ok(Self::V1Beta1), + "stackable.tech/v1" => Ok(Self::V1), + _ => { + Err(::stackable_versioned::UnknownDesiredApiVersionError { + api_version: api_version.to_owned(), + }) + } + } + } } #[automatically_derived] #[derive( diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@crate_overrides.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@crate_overrides.rs.snap index 3853ad39..4a5d9407 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@crate_overrides.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@crate_overrides.rs.snap @@ -124,7 +124,7 @@ impl Foo { v1beta1::Foo as ::kube::core::CustomResourceExt > ::crd(), < v1::Foo as ::kube::core::CustomResourceExt > ::crd() ], - stored_apiversion.as_str(), + stored_apiversion.as_version_str(), ) } ///Tries to convert a list of objects of kind [`Foo`] to the desired API version @@ -152,10 +152,9 @@ impl Foo { .into_review(); } }; - let desired_api_version = request.desired_api_version.as_str(); let response = match Self::convert_objects( request.objects, - desired_api_version, + &request.desired_api_version, ) { ::std::result::Result::Ok(converted_objects) => { ::kube::core::conversion::ConversionResponse { @@ -191,14 +190,18 @@ impl Foo { ::std::vec::Vec<::serde_json::Value>, ::stackable_versioned::ConvertObjectError, > { + let desired_api_version = FooVersion::from_api_version(desired_api_version) + .map_err(|source| ::stackable_versioned::ConvertObjectError::ParseDesiredApiVersion { + source, + })?; let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); for object in objects { - let current_object = Self::from_json_value(object.clone()) + let current_object = Self::from_json_object(object.clone()) .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { source, })?; match (current_object, desired_api_version) { - (Self::V1Alpha1(__sv_foo), "v1beta1") => { + (Self::V1Alpha1(__sv_foo), FooVersion::V1Beta1) => { let converted: v1beta1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1Beta1(v1beta1::Foo { metadata: __sv_foo.metadata, @@ -211,7 +214,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1Alpha1(__sv_foo), "v1") => { + (Self::V1Alpha1(__sv_foo), FooVersion::V1) => { let converted: v1beta1::FooSpec = __sv_foo.spec.into(); let converted: v1::FooSpec = converted.into(); let desired_object = Self::V1(v1::Foo { @@ -225,7 +228,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1Beta1(__sv_foo), "v1alpha1") => { + (Self::V1Beta1(__sv_foo), FooVersion::V1Alpha1) => { let converted: v1alpha1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1Alpha1(v1alpha1::Foo { metadata: __sv_foo.metadata, @@ -238,7 +241,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1Beta1(__sv_foo), "v1") => { + (Self::V1Beta1(__sv_foo), FooVersion::V1) => { let converted: v1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1(v1::Foo { metadata: __sv_foo.metadata, @@ -251,7 +254,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1(__sv_foo), "v1alpha1") => { + (Self::V1(__sv_foo), FooVersion::V1Alpha1) => { let converted: v1beta1::FooSpec = __sv_foo.spec.into(); let converted: v1alpha1::FooSpec = converted.into(); let desired_object = Self::V1Alpha1(v1alpha1::Foo { @@ -265,7 +268,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1(__sv_foo), "v1beta1") => { + (Self::V1(__sv_foo), FooVersion::V1Beta1) => { let converted: v1beta1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1Beta1(v1beta1::Foo { metadata: __sv_foo.metadata, @@ -283,31 +286,50 @@ impl Foo { } ::std::result::Result::Ok(converted_objects) } - fn from_json_value( - value: ::serde_json::Value, + fn from_json_object( + object_value: ::serde_json::Value, ) -> ::std::result::Result { - let api_version = value + let object_kind = object_value + .get("kind") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "kind".to_owned(), + })? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "kind".to_owned(), + })?; + if object_kind != "Foo" { + return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { + kind: object_kind.to_owned(), + expected: "Foo".to_owned(), + }); + } + let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "apiVersion".to_owned(), + })? .as_str() - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "apiVersion".to_owned(), + })?; let object = match api_version { - "foo.example.org/v1alpha1" | "v1alpha1" => { - let object = ::serde_json::from_value(value) + "foo.example.org/v1alpha1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; Self::V1Alpha1(object) } - "foo.example.org/v1beta1" | "v1beta1" => { - let object = ::serde_json::from_value(value) + "foo.example.org/v1beta1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; Self::V1Beta1(object) } - "foo.example.org/v1" | "v1" => { - let object = ::serde_json::from_value(value) + "foo.example.org/v1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; @@ -332,6 +354,7 @@ impl Foo { } } #[automatically_derived] +#[derive(Copy, Clone, Debug)] pub enum FooVersion { V1Alpha1, V1Beta1, @@ -343,16 +366,37 @@ impl ::std::fmt::Display for FooVersion { &self, f: &mut ::std::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { - f.write_str(self.as_str()) + f.write_str(self.as_version_str()) } } #[automatically_derived] impl FooVersion { - pub fn as_str(&self) -> &str { + pub fn as_version_str(&self) -> &str { match self { V1Alpha1 => "v1alpha1", V1Beta1 => "v1beta1", V1 => "v1", } } + pub fn as_api_version_str(&self) -> &str { + match self { + V1Alpha1 => "foo.example.org/v1alpha1", + V1Beta1 => "foo.example.org/v1beta1", + V1 => "foo.example.org/v1", + } + } + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "foo.example.org/v1alpha1" => Ok(Self::V1Alpha1), + "foo.example.org/v1beta1" => Ok(Self::V1Beta1), + "foo.example.org/v1" => Ok(Self::V1), + _ => { + Err(::stackable_versioned::UnknownDesiredApiVersionError { + api_version: api_version.to_owned(), + }) + } + } + } } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module.rs.snap index d63333e7..9c3d2a6d 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module.rs.snap @@ -296,7 +296,7 @@ impl Foo { as ::kube::core::CustomResourceExt > ::crd(), < v2alpha1::Foo as ::kube::core::CustomResourceExt > ::crd() ], - stored_apiversion.as_str(), + stored_apiversion.as_version_str(), ) } ///Tries to convert a list of objects of kind [`Foo`] to the desired API version @@ -324,10 +324,9 @@ impl Foo { .into_review(); } }; - let desired_api_version = request.desired_api_version.as_str(); let response = match Self::convert_objects( request.objects, - desired_api_version, + &request.desired_api_version, ) { ::std::result::Result::Ok(converted_objects) => { ::kube::core::conversion::ConversionResponse { @@ -363,14 +362,18 @@ impl Foo { ::std::vec::Vec<::serde_json::Value>, ::stackable_versioned::ConvertObjectError, > { + let desired_api_version = FooVersion::from_api_version(desired_api_version) + .map_err(|source| ::stackable_versioned::ConvertObjectError::ParseDesiredApiVersion { + source, + })?; let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); for object in objects { - let current_object = Self::from_json_value(object.clone()) + let current_object = Self::from_json_object(object.clone()) .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { source, })?; match (current_object, desired_api_version) { - (Self::V1Alpha1(__sv_foo), "v1") => { + (Self::V1Alpha1(__sv_foo), FooVersion::V1) => { let converted: v1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1(v1::Foo { metadata: __sv_foo.metadata, @@ -383,7 +386,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1Alpha1(__sv_foo), "v2alpha1") => { + (Self::V1Alpha1(__sv_foo), FooVersion::V2Alpha1) => { let converted: v1::FooSpec = __sv_foo.spec.into(); let converted: v2alpha1::FooSpec = converted.into(); let desired_object = Self::V2Alpha1(v2alpha1::Foo { @@ -397,7 +400,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1(__sv_foo), "v1alpha1") => { + (Self::V1(__sv_foo), FooVersion::V1Alpha1) => { let converted: v1alpha1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1Alpha1(v1alpha1::Foo { metadata: __sv_foo.metadata, @@ -410,7 +413,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V1(__sv_foo), "v2alpha1") => { + (Self::V1(__sv_foo), FooVersion::V2Alpha1) => { let converted: v2alpha1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V2Alpha1(v2alpha1::Foo { metadata: __sv_foo.metadata, @@ -423,7 +426,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V2Alpha1(__sv_foo), "v1alpha1") => { + (Self::V2Alpha1(__sv_foo), FooVersion::V1Alpha1) => { let converted: v1::FooSpec = __sv_foo.spec.into(); let converted: v1alpha1::FooSpec = converted.into(); let desired_object = Self::V1Alpha1(v1alpha1::Foo { @@ -437,7 +440,7 @@ impl Foo { })?; converted_objects.push(desired_object); } - (Self::V2Alpha1(__sv_foo), "v1") => { + (Self::V2Alpha1(__sv_foo), FooVersion::V1) => { let converted: v1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1(v1::Foo { metadata: __sv_foo.metadata, @@ -455,31 +458,50 @@ impl Foo { } ::std::result::Result::Ok(converted_objects) } - fn from_json_value( - value: ::serde_json::Value, + fn from_json_object( + object_value: ::serde_json::Value, ) -> ::std::result::Result { - let api_version = value + let object_kind = object_value + .get("kind") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "kind".to_owned(), + })? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "kind".to_owned(), + })?; + if object_kind != "Foo" { + return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { + kind: object_kind.to_owned(), + expected: "Foo".to_owned(), + }); + } + let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "apiVersion".to_owned(), + })? .as_str() - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "apiVersion".to_owned(), + })?; let object = match api_version { - "foo.example.org/v1alpha1" | "v1alpha1" => { - let object = ::serde_json::from_value(value) + "foo.example.org/v1alpha1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; Self::V1Alpha1(object) } - "foo.example.org/v1" | "v1" => { - let object = ::serde_json::from_value(value) + "foo.example.org/v1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; Self::V1(object) } - "foo.example.org/v2alpha1" | "v2alpha1" => { - let object = ::serde_json::from_value(value) + "foo.example.org/v2alpha1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; @@ -504,6 +526,7 @@ impl Foo { } } #[automatically_derived] +#[derive(Copy, Clone, Debug)] pub(crate) enum FooVersion { V1Alpha1, V1, @@ -515,18 +538,39 @@ impl ::std::fmt::Display for FooVersion { &self, f: &mut ::std::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { - f.write_str(self.as_str()) + f.write_str(self.as_version_str()) } } #[automatically_derived] impl FooVersion { - pub fn as_str(&self) -> &str { + pub fn as_version_str(&self) -> &str { match self { V1Alpha1 => "v1alpha1", V1 => "v1", V2Alpha1 => "v2alpha1", } } + pub fn as_api_version_str(&self) -> &str { + match self { + V1Alpha1 => "foo.example.org/v1alpha1", + V1 => "foo.example.org/v1", + V2Alpha1 => "foo.example.org/v2alpha1", + } + } + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "foo.example.org/v1alpha1" => Ok(Self::V1Alpha1), + "foo.example.org/v1" => Ok(Self::V1), + "foo.example.org/v2alpha1" => Ok(Self::V2Alpha1), + _ => { + Err(::stackable_versioned::UnknownDesiredApiVersionError { + api_version: api_version.to_owned(), + }) + } + } + } } #[automatically_derived] pub(crate) enum Bar { @@ -549,7 +593,7 @@ impl Bar { as ::kube::core::CustomResourceExt > ::crd(), < v2alpha1::Bar as ::kube::core::CustomResourceExt > ::crd() ], - stored_apiversion.as_str(), + stored_apiversion.as_version_str(), ) } ///Tries to convert a list of objects of kind [`Bar`] to the desired API version @@ -577,10 +621,9 @@ impl Bar { .into_review(); } }; - let desired_api_version = request.desired_api_version.as_str(); let response = match Self::convert_objects( request.objects, - desired_api_version, + &request.desired_api_version, ) { ::std::result::Result::Ok(converted_objects) => { ::kube::core::conversion::ConversionResponse { @@ -616,14 +659,18 @@ impl Bar { ::std::vec::Vec<::serde_json::Value>, ::stackable_versioned::ConvertObjectError, > { + let desired_api_version = BarVersion::from_api_version(desired_api_version) + .map_err(|source| ::stackable_versioned::ConvertObjectError::ParseDesiredApiVersion { + source, + })?; let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); for object in objects { - let current_object = Self::from_json_value(object.clone()) + let current_object = Self::from_json_object(object.clone()) .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { source, })?; match (current_object, desired_api_version) { - (Self::V1Alpha1(__sv_bar), "v1") => { + (Self::V1Alpha1(__sv_bar), BarVersion::V1) => { let converted: v1::BarSpec = __sv_bar.spec.into(); let desired_object = Self::V1(v1::Bar { metadata: __sv_bar.metadata, @@ -636,7 +683,7 @@ impl Bar { })?; converted_objects.push(desired_object); } - (Self::V1Alpha1(__sv_bar), "v2alpha1") => { + (Self::V1Alpha1(__sv_bar), BarVersion::V2Alpha1) => { let converted: v1::BarSpec = __sv_bar.spec.into(); let converted: v2alpha1::BarSpec = converted.into(); let desired_object = Self::V2Alpha1(v2alpha1::Bar { @@ -650,7 +697,7 @@ impl Bar { })?; converted_objects.push(desired_object); } - (Self::V1(__sv_bar), "v1alpha1") => { + (Self::V1(__sv_bar), BarVersion::V1Alpha1) => { let converted: v1alpha1::BarSpec = __sv_bar.spec.into(); let desired_object = Self::V1Alpha1(v1alpha1::Bar { metadata: __sv_bar.metadata, @@ -663,7 +710,7 @@ impl Bar { })?; converted_objects.push(desired_object); } - (Self::V1(__sv_bar), "v2alpha1") => { + (Self::V1(__sv_bar), BarVersion::V2Alpha1) => { let converted: v2alpha1::BarSpec = __sv_bar.spec.into(); let desired_object = Self::V2Alpha1(v2alpha1::Bar { metadata: __sv_bar.metadata, @@ -676,7 +723,7 @@ impl Bar { })?; converted_objects.push(desired_object); } - (Self::V2Alpha1(__sv_bar), "v1alpha1") => { + (Self::V2Alpha1(__sv_bar), BarVersion::V1Alpha1) => { let converted: v1::BarSpec = __sv_bar.spec.into(); let converted: v1alpha1::BarSpec = converted.into(); let desired_object = Self::V1Alpha1(v1alpha1::Bar { @@ -690,7 +737,7 @@ impl Bar { })?; converted_objects.push(desired_object); } - (Self::V2Alpha1(__sv_bar), "v1") => { + (Self::V2Alpha1(__sv_bar), BarVersion::V1) => { let converted: v1::BarSpec = __sv_bar.spec.into(); let desired_object = Self::V1(v1::Bar { metadata: __sv_bar.metadata, @@ -708,31 +755,50 @@ impl Bar { } ::std::result::Result::Ok(converted_objects) } - fn from_json_value( - value: ::serde_json::Value, + fn from_json_object( + object_value: ::serde_json::Value, ) -> ::std::result::Result { - let api_version = value + let object_kind = object_value + .get("kind") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "kind".to_owned(), + })? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "kind".to_owned(), + })?; + if object_kind != "Bar" { + return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { + kind: object_kind.to_owned(), + expected: "Bar".to_owned(), + }); + } + let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "apiVersion".to_owned(), + })? .as_str() - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "apiVersion".to_owned(), + })?; let object = match api_version { - "bar.example.org/v1alpha1" | "v1alpha1" => { - let object = ::serde_json::from_value(value) + "bar.example.org/v1alpha1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; Self::V1Alpha1(object) } - "bar.example.org/v1" | "v1" => { - let object = ::serde_json::from_value(value) + "bar.example.org/v1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; Self::V1(object) } - "bar.example.org/v2alpha1" | "v2alpha1" => { - let object = ::serde_json::from_value(value) + "bar.example.org/v2alpha1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; @@ -757,6 +823,7 @@ impl Bar { } } #[automatically_derived] +#[derive(Copy, Clone, Debug)] pub(crate) enum BarVersion { V1Alpha1, V1, @@ -768,16 +835,37 @@ impl ::std::fmt::Display for BarVersion { &self, f: &mut ::std::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { - f.write_str(self.as_str()) + f.write_str(self.as_version_str()) } } #[automatically_derived] impl BarVersion { - pub fn as_str(&self) -> &str { + pub fn as_version_str(&self) -> &str { match self { V1Alpha1 => "v1alpha1", V1 => "v1", V2Alpha1 => "v2alpha1", } } + pub fn as_api_version_str(&self) -> &str { + match self { + V1Alpha1 => "bar.example.org/v1alpha1", + V1 => "bar.example.org/v1", + V2Alpha1 => "bar.example.org/v2alpha1", + } + } + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "bar.example.org/v1alpha1" => Ok(Self::V1Alpha1), + "bar.example.org/v1" => Ok(Self::V1), + "bar.example.org/v2alpha1" => Ok(Self::V2Alpha1), + _ => { + Err(::stackable_versioned::UnknownDesiredApiVersionError { + api_version: api_version.to_owned(), + }) + } + } + } } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module_preserve.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module_preserve.rs.snap index 6a74e8b0..6208b104 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module_preserve.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@module_preserve.rs.snap @@ -277,7 +277,7 @@ pub(crate) mod versioned { v1::Foo as ::kube::core::CustomResourceExt > ::crd(), < v2alpha1::Foo as ::kube::core::CustomResourceExt > ::crd() ], - stored_apiversion.as_str(), + stored_apiversion.as_version_str(), ) } ///Tries to convert a list of objects of kind [`Foo`] to the desired API version @@ -305,10 +305,9 @@ pub(crate) mod versioned { .into_review(); } }; - let desired_api_version = request.desired_api_version.as_str(); let response = match Self::convert_objects( request.objects, - desired_api_version, + &request.desired_api_version, ) { ::std::result::Result::Ok(converted_objects) => { ::kube::core::conversion::ConversionResponse { @@ -344,14 +343,18 @@ pub(crate) mod versioned { ::std::vec::Vec<::serde_json::Value>, ::stackable_versioned::ConvertObjectError, > { + let desired_api_version = FooVersion::from_api_version(desired_api_version) + .map_err(|source| ::stackable_versioned::ConvertObjectError::ParseDesiredApiVersion { + source, + })?; let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); for object in objects { - let current_object = Self::from_json_value(object.clone()) + let current_object = Self::from_json_object(object.clone()) .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { source, })?; match (current_object, desired_api_version) { - (Self::V1Alpha1(__sv_foo), "v1") => { + (Self::V1Alpha1(__sv_foo), FooVersion::V1) => { let converted: v1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1(v1::Foo { metadata: __sv_foo.metadata, @@ -364,7 +367,7 @@ pub(crate) mod versioned { })?; converted_objects.push(desired_object); } - (Self::V1Alpha1(__sv_foo), "v2alpha1") => { + (Self::V1Alpha1(__sv_foo), FooVersion::V2Alpha1) => { let converted: v1::FooSpec = __sv_foo.spec.into(); let converted: v2alpha1::FooSpec = converted.into(); let desired_object = Self::V2Alpha1(v2alpha1::Foo { @@ -378,7 +381,7 @@ pub(crate) mod versioned { })?; converted_objects.push(desired_object); } - (Self::V1(__sv_foo), "v1alpha1") => { + (Self::V1(__sv_foo), FooVersion::V1Alpha1) => { let converted: v1alpha1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1Alpha1(v1alpha1::Foo { metadata: __sv_foo.metadata, @@ -391,7 +394,7 @@ pub(crate) mod versioned { })?; converted_objects.push(desired_object); } - (Self::V1(__sv_foo), "v2alpha1") => { + (Self::V1(__sv_foo), FooVersion::V2Alpha1) => { let converted: v2alpha1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V2Alpha1(v2alpha1::Foo { metadata: __sv_foo.metadata, @@ -404,7 +407,7 @@ pub(crate) mod versioned { })?; converted_objects.push(desired_object); } - (Self::V2Alpha1(__sv_foo), "v1alpha1") => { + (Self::V2Alpha1(__sv_foo), FooVersion::V1Alpha1) => { let converted: v1::FooSpec = __sv_foo.spec.into(); let converted: v1alpha1::FooSpec = converted.into(); let desired_object = Self::V1Alpha1(v1alpha1::Foo { @@ -418,7 +421,7 @@ pub(crate) mod versioned { })?; converted_objects.push(desired_object); } - (Self::V2Alpha1(__sv_foo), "v1") => { + (Self::V2Alpha1(__sv_foo), FooVersion::V1) => { let converted: v1::FooSpec = __sv_foo.spec.into(); let desired_object = Self::V1(v1::Foo { metadata: __sv_foo.metadata, @@ -436,31 +439,50 @@ pub(crate) mod versioned { } ::std::result::Result::Ok(converted_objects) } - fn from_json_value( - value: ::serde_json::Value, + fn from_json_object( + object_value: ::serde_json::Value, ) -> ::std::result::Result { - let api_version = value + let object_kind = object_value + .get("kind") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "kind".to_owned(), + })? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "kind".to_owned(), + })?; + if object_kind != "Foo" { + return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { + kind: object_kind.to_owned(), + expected: "Foo".to_owned(), + }); + } + let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "apiVersion".to_owned(), + })? .as_str() - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "apiVersion".to_owned(), + })?; let object = match api_version { - "foo.example.org/v1alpha1" | "v1alpha1" => { - let object = ::serde_json::from_value(value) + "foo.example.org/v1alpha1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; Self::V1Alpha1(object) } - "foo.example.org/v1" | "v1" => { - let object = ::serde_json::from_value(value) + "foo.example.org/v1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; Self::V1(object) } - "foo.example.org/v2alpha1" | "v2alpha1" => { - let object = ::serde_json::from_value(value) + "foo.example.org/v2alpha1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; @@ -484,6 +506,7 @@ pub(crate) mod versioned { } } } + #[derive(Copy, Clone, Debug)] pub enum FooVersion { V1Alpha1, V1, @@ -494,17 +517,38 @@ pub(crate) mod versioned { &self, f: &mut ::std::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { - f.write_str(self.as_str()) + f.write_str(self.as_version_str()) } } impl FooVersion { - pub fn as_str(&self) -> &str { + pub fn as_version_str(&self) -> &str { match self { V1Alpha1 => "v1alpha1", V1 => "v1", V2Alpha1 => "v2alpha1", } } + pub fn as_api_version_str(&self) -> &str { + match self { + V1Alpha1 => "foo.example.org/v1alpha1", + V1 => "foo.example.org/v1", + V2Alpha1 => "foo.example.org/v2alpha1", + } + } + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "foo.example.org/v1alpha1" => Ok(Self::V1Alpha1), + "foo.example.org/v1" => Ok(Self::V1), + "foo.example.org/v2alpha1" => Ok(Self::V2Alpha1), + _ => { + Err(::stackable_versioned::UnknownDesiredApiVersionError { + api_version: api_version.to_owned(), + }) + } + } + } } pub enum Bar { V1Alpha1(v1alpha1::Bar), @@ -525,7 +569,7 @@ pub(crate) mod versioned { v1::Bar as ::kube::core::CustomResourceExt > ::crd(), < v2alpha1::Bar as ::kube::core::CustomResourceExt > ::crd() ], - stored_apiversion.as_str(), + stored_apiversion.as_version_str(), ) } ///Tries to convert a list of objects of kind [`Bar`] to the desired API version @@ -553,10 +597,9 @@ pub(crate) mod versioned { .into_review(); } }; - let desired_api_version = request.desired_api_version.as_str(); let response = match Self::convert_objects( request.objects, - desired_api_version, + &request.desired_api_version, ) { ::std::result::Result::Ok(converted_objects) => { ::kube::core::conversion::ConversionResponse { @@ -592,14 +635,18 @@ pub(crate) mod versioned { ::std::vec::Vec<::serde_json::Value>, ::stackable_versioned::ConvertObjectError, > { + let desired_api_version = BarVersion::from_api_version(desired_api_version) + .map_err(|source| ::stackable_versioned::ConvertObjectError::ParseDesiredApiVersion { + source, + })?; let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); for object in objects { - let current_object = Self::from_json_value(object.clone()) + let current_object = Self::from_json_object(object.clone()) .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { source, })?; match (current_object, desired_api_version) { - (Self::V1Alpha1(__sv_bar), "v1") => { + (Self::V1Alpha1(__sv_bar), BarVersion::V1) => { let converted: v1::BarSpec = __sv_bar.spec.into(); let desired_object = Self::V1(v1::Bar { metadata: __sv_bar.metadata, @@ -612,7 +659,7 @@ pub(crate) mod versioned { })?; converted_objects.push(desired_object); } - (Self::V1Alpha1(__sv_bar), "v2alpha1") => { + (Self::V1Alpha1(__sv_bar), BarVersion::V2Alpha1) => { let converted: v1::BarSpec = __sv_bar.spec.into(); let converted: v2alpha1::BarSpec = converted.into(); let desired_object = Self::V2Alpha1(v2alpha1::Bar { @@ -626,7 +673,7 @@ pub(crate) mod versioned { })?; converted_objects.push(desired_object); } - (Self::V1(__sv_bar), "v1alpha1") => { + (Self::V1(__sv_bar), BarVersion::V1Alpha1) => { let converted: v1alpha1::BarSpec = __sv_bar.spec.into(); let desired_object = Self::V1Alpha1(v1alpha1::Bar { metadata: __sv_bar.metadata, @@ -639,7 +686,7 @@ pub(crate) mod versioned { })?; converted_objects.push(desired_object); } - (Self::V1(__sv_bar), "v2alpha1") => { + (Self::V1(__sv_bar), BarVersion::V2Alpha1) => { let converted: v2alpha1::BarSpec = __sv_bar.spec.into(); let desired_object = Self::V2Alpha1(v2alpha1::Bar { metadata: __sv_bar.metadata, @@ -652,7 +699,7 @@ pub(crate) mod versioned { })?; converted_objects.push(desired_object); } - (Self::V2Alpha1(__sv_bar), "v1alpha1") => { + (Self::V2Alpha1(__sv_bar), BarVersion::V1Alpha1) => { let converted: v1::BarSpec = __sv_bar.spec.into(); let converted: v1alpha1::BarSpec = converted.into(); let desired_object = Self::V1Alpha1(v1alpha1::Bar { @@ -666,7 +713,7 @@ pub(crate) mod versioned { })?; converted_objects.push(desired_object); } - (Self::V2Alpha1(__sv_bar), "v1") => { + (Self::V2Alpha1(__sv_bar), BarVersion::V1) => { let converted: v1::BarSpec = __sv_bar.spec.into(); let desired_object = Self::V1(v1::Bar { metadata: __sv_bar.metadata, @@ -684,31 +731,50 @@ pub(crate) mod versioned { } ::std::result::Result::Ok(converted_objects) } - fn from_json_value( - value: ::serde_json::Value, + fn from_json_object( + object_value: ::serde_json::Value, ) -> ::std::result::Result { - let api_version = value + let object_kind = object_value + .get("kind") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "kind".to_owned(), + })? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "kind".to_owned(), + })?; + if object_kind != "Bar" { + return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { + kind: object_kind.to_owned(), + expected: "Bar".to_owned(), + }); + } + let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "apiVersion".to_owned(), + })? .as_str() - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "apiVersion".to_owned(), + })?; let object = match api_version { - "bar.example.org/v1alpha1" | "v1alpha1" => { - let object = ::serde_json::from_value(value) + "bar.example.org/v1alpha1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; Self::V1Alpha1(object) } - "bar.example.org/v1" | "v1" => { - let object = ::serde_json::from_value(value) + "bar.example.org/v1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; Self::V1(object) } - "bar.example.org/v2alpha1" | "v2alpha1" => { - let object = ::serde_json::from_value(value) + "bar.example.org/v2alpha1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; @@ -732,6 +798,7 @@ pub(crate) mod versioned { } } } + #[derive(Copy, Clone, Debug)] pub enum BarVersion { V1Alpha1, V1, @@ -742,16 +809,37 @@ pub(crate) mod versioned { &self, f: &mut ::std::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { - f.write_str(self.as_str()) + f.write_str(self.as_version_str()) } } impl BarVersion { - pub fn as_str(&self) -> &str { + pub fn as_version_str(&self) -> &str { match self { V1Alpha1 => "v1alpha1", V1 => "v1", V2Alpha1 => "v2alpha1", } } + pub fn as_api_version_str(&self) -> &str { + match self { + V1Alpha1 => "bar.example.org/v1alpha1", + V1 => "bar.example.org/v1", + V2Alpha1 => "bar.example.org/v2alpha1", + } + } + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "bar.example.org/v1alpha1" => Ok(Self::V1Alpha1), + "bar.example.org/v1" => Ok(Self::V1), + "bar.example.org/v2alpha1" => Ok(Self::V2Alpha1), + _ => { + Err(::stackable_versioned::UnknownDesiredApiVersionError { + api_version: api_version.to_owned(), + }) + } + } + } } } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@renamed_kind.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@renamed_kind.rs.snap index d284f43c..e71e3bfd 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@renamed_kind.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@renamed_kind.rs.snap @@ -109,7 +109,7 @@ impl FooBar { v1beta1::FooBar as ::kube::core::CustomResourceExt > ::crd(), < v1::FooBar as ::kube::core::CustomResourceExt > ::crd() ], - stored_apiversion.as_str(), + stored_apiversion.as_version_str(), ) } ///Tries to convert a list of objects of kind [`FooBar`] to the desired API version @@ -137,10 +137,9 @@ impl FooBar { .into_review(); } }; - let desired_api_version = request.desired_api_version.as_str(); let response = match Self::convert_objects( request.objects, - desired_api_version, + &request.desired_api_version, ) { ::std::result::Result::Ok(converted_objects) => { ::kube::core::conversion::ConversionResponse { @@ -176,14 +175,18 @@ impl FooBar { ::std::vec::Vec<::serde_json::Value>, ::stackable_versioned::ConvertObjectError, > { + let desired_api_version = FooBarVersion::from_api_version(desired_api_version) + .map_err(|source| ::stackable_versioned::ConvertObjectError::ParseDesiredApiVersion { + source, + })?; let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); for object in objects { - let current_object = Self::from_json_value(object.clone()) + let current_object = Self::from_json_object(object.clone()) .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { source, })?; match (current_object, desired_api_version) { - (Self::V1Alpha1(__sv_foobar), "v1beta1") => { + (Self::V1Alpha1(__sv_foobar), FooBarVersion::V1Beta1) => { let converted: v1beta1::FooSpec = __sv_foobar.spec.into(); let desired_object = Self::V1Beta1(v1beta1::FooBar { metadata: __sv_foobar.metadata, @@ -196,7 +199,7 @@ impl FooBar { })?; converted_objects.push(desired_object); } - (Self::V1Alpha1(__sv_foobar), "v1") => { + (Self::V1Alpha1(__sv_foobar), FooBarVersion::V1) => { let converted: v1beta1::FooSpec = __sv_foobar.spec.into(); let converted: v1::FooSpec = converted.into(); let desired_object = Self::V1(v1::FooBar { @@ -210,7 +213,7 @@ impl FooBar { })?; converted_objects.push(desired_object); } - (Self::V1Beta1(__sv_foobar), "v1alpha1") => { + (Self::V1Beta1(__sv_foobar), FooBarVersion::V1Alpha1) => { let converted: v1alpha1::FooSpec = __sv_foobar.spec.into(); let desired_object = Self::V1Alpha1(v1alpha1::FooBar { metadata: __sv_foobar.metadata, @@ -223,7 +226,7 @@ impl FooBar { })?; converted_objects.push(desired_object); } - (Self::V1Beta1(__sv_foobar), "v1") => { + (Self::V1Beta1(__sv_foobar), FooBarVersion::V1) => { let converted: v1::FooSpec = __sv_foobar.spec.into(); let desired_object = Self::V1(v1::FooBar { metadata: __sv_foobar.metadata, @@ -236,7 +239,7 @@ impl FooBar { })?; converted_objects.push(desired_object); } - (Self::V1(__sv_foobar), "v1alpha1") => { + (Self::V1(__sv_foobar), FooBarVersion::V1Alpha1) => { let converted: v1beta1::FooSpec = __sv_foobar.spec.into(); let converted: v1alpha1::FooSpec = converted.into(); let desired_object = Self::V1Alpha1(v1alpha1::FooBar { @@ -250,7 +253,7 @@ impl FooBar { })?; converted_objects.push(desired_object); } - (Self::V1(__sv_foobar), "v1beta1") => { + (Self::V1(__sv_foobar), FooBarVersion::V1Beta1) => { let converted: v1beta1::FooSpec = __sv_foobar.spec.into(); let desired_object = Self::V1Beta1(v1beta1::FooBar { metadata: __sv_foobar.metadata, @@ -268,31 +271,50 @@ impl FooBar { } ::std::result::Result::Ok(converted_objects) } - fn from_json_value( - value: ::serde_json::Value, + fn from_json_object( + object_value: ::serde_json::Value, ) -> ::std::result::Result { - let api_version = value + let object_kind = object_value + .get("kind") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "kind".to_owned(), + })? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "kind".to_owned(), + })?; + if object_kind != "FooBar" { + return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { + kind: object_kind.to_owned(), + expected: "FooBar".to_owned(), + }); + } + let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "apiVersion".to_owned(), + })? .as_str() - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "apiVersion".to_owned(), + })?; let object = match api_version { - "stackable.tech/v1alpha1" | "v1alpha1" => { - let object = ::serde_json::from_value(value) + "stackable.tech/v1alpha1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; Self::V1Alpha1(object) } - "stackable.tech/v1beta1" | "v1beta1" => { - let object = ::serde_json::from_value(value) + "stackable.tech/v1beta1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; Self::V1Beta1(object) } - "stackable.tech/v1" | "v1" => { - let object = ::serde_json::from_value(value) + "stackable.tech/v1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; @@ -317,6 +339,7 @@ impl FooBar { } } #[automatically_derived] +#[derive(Copy, Clone, Debug)] pub enum FooBarVersion { V1Alpha1, V1Beta1, @@ -328,16 +351,37 @@ impl ::std::fmt::Display for FooBarVersion { &self, f: &mut ::std::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { - f.write_str(self.as_str()) + f.write_str(self.as_version_str()) } } #[automatically_derived] impl FooBarVersion { - pub fn as_str(&self) -> &str { + pub fn as_version_str(&self) -> &str { match self { V1Alpha1 => "v1alpha1", V1Beta1 => "v1beta1", V1 => "v1", } } + pub fn as_api_version_str(&self) -> &str { + match self { + V1Alpha1 => "stackable.tech/v1alpha1", + V1Beta1 => "stackable.tech/v1beta1", + V1 => "stackable.tech/v1", + } + } + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "stackable.tech/v1alpha1" => Ok(Self::V1Alpha1), + "stackable.tech/v1beta1" => Ok(Self::V1Beta1), + "stackable.tech/v1" => Ok(Self::V1), + _ => { + Err(::stackable_versioned::UnknownDesiredApiVersionError { + api_version: api_version.to_owned(), + }) + } + } + } } diff --git a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@shortnames.rs.snap b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@shortnames.rs.snap index 94e14e24..111436af 100644 --- a/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@shortnames.rs.snap +++ b/crates/stackable-versioned-macros/tests/snapshots/stackable_versioned_macros__snapshot_tests__k8s@shortnames.rs.snap @@ -38,7 +38,7 @@ impl Foo { > { ::kube::core::crd::merge_crds( vec![< v1alpha1::Foo as ::kube::core::CustomResourceExt > ::crd()], - stored_apiversion.as_str(), + stored_apiversion.as_version_str(), ) } ///Tries to convert a list of objects of kind [`Foo`] to the desired API version @@ -66,10 +66,9 @@ impl Foo { .into_review(); } }; - let desired_api_version = request.desired_api_version.as_str(); let response = match Self::convert_objects( request.objects, - desired_api_version, + &request.desired_api_version, ) { ::std::result::Result::Ok(converted_objects) => { ::kube::core::conversion::ConversionResponse { @@ -105,9 +104,13 @@ impl Foo { ::std::vec::Vec<::serde_json::Value>, ::stackable_versioned::ConvertObjectError, > { + let desired_api_version = FooVersion::from_api_version(desired_api_version) + .map_err(|source| ::stackable_versioned::ConvertObjectError::ParseDesiredApiVersion { + source, + })?; let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len()); for object in objects { - let current_object = Self::from_json_value(object.clone()) + let current_object = Self::from_json_object(object.clone()) .map_err(|source| ::stackable_versioned::ConvertObjectError::Parse { source, })?; @@ -117,17 +120,36 @@ impl Foo { } ::std::result::Result::Ok(converted_objects) } - fn from_json_value( - value: ::serde_json::Value, + fn from_json_object( + object_value: ::serde_json::Value, ) -> ::std::result::Result { - let api_version = value + let object_kind = object_value + .get("kind") + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "kind".to_owned(), + })? + .as_str() + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "kind".to_owned(), + })?; + if object_kind != "Foo" { + return Err(::stackable_versioned::ParseObjectError::UnexpectedKind { + kind: object_kind.to_owned(), + expected: "Foo".to_owned(), + }); + } + let api_version = object_value .get("apiVersion") - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotPresent)? + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldMissing { + field: "apiVersion".to_owned(), + })? .as_str() - .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr)?; + .ok_or_else(|| ::stackable_versioned::ParseObjectError::FieldNotStr { + field: "apiVersion".to_owned(), + })?; let object = match api_version { - "stackable.tech/v1alpha1" | "v1alpha1" => { - let object = ::serde_json::from_value(value) + "stackable.tech/v1alpha1" => { + let object = ::serde_json::from_value(object_value) .map_err(|source| ::stackable_versioned::ParseObjectError::Deserialize { source, })?; @@ -150,6 +172,7 @@ impl Foo { } } #[automatically_derived] +#[derive(Copy, Clone, Debug)] pub(crate) enum FooVersion { V1Alpha1, } @@ -159,14 +182,31 @@ impl ::std::fmt::Display for FooVersion { &self, f: &mut ::std::fmt::Formatter<'_>, ) -> ::std::result::Result<(), ::std::fmt::Error> { - f.write_str(self.as_str()) + f.write_str(self.as_version_str()) } } #[automatically_derived] impl FooVersion { - pub fn as_str(&self) -> &str { + pub fn as_version_str(&self) -> &str { match self { V1Alpha1 => "v1alpha1", } } + pub fn as_api_version_str(&self) -> &str { + match self { + V1Alpha1 => "stackable.tech/v1alpha1", + } + } + pub fn from_api_version( + api_version: &str, + ) -> Result { + match api_version { + "stackable.tech/v1alpha1" => Ok(Self::V1Alpha1), + _ => { + Err(::stackable_versioned::UnknownDesiredApiVersionError { + api_version: api_version.to_owned(), + }) + } + } + } } diff --git a/crates/stackable-versioned/CHANGELOG.md b/crates/stackable-versioned/CHANGELOG.md index 91844951..58968d11 100644 --- a/crates/stackable-versioned/CHANGELOG.md +++ b/crates/stackable-versioned/CHANGELOG.md @@ -6,9 +6,11 @@ All notable changes to this project will be documented in this file. ### Added -- Add support for CRD conversions via ConversionReviews ([#1050]). +- Add support for CRD conversions via ConversionReviews ([#1050], [#1061]). - Add new `try_convert` function to convert objects received via a ConversionReview. - Add new `enable_tracing` option to `#[versioned(k8s(options(...)))]`. + - Add a `Version` enum with `from_api_version`, `as_version_str` and `as_api_version_str` + functions. - Implement basic ground work for downgrading custom resources ([#1033]). - Emit `From` implementations to downgrade custom resource specs. - Emit a status struct to be able to track values required during downgrades and upgrades of @@ -52,6 +54,7 @@ All notable changes to this project will be documented in this file. [#1046]: https://github.com/stackabletech/operator-rs/pull/1046 [#1050]: https://github.com/stackabletech/operator-rs/pull/1050 [#1059]: https://github.com/stackabletech/operator-rs/pull/1059 +[#1061]: https://github.com/stackabletech/operator-rs/pull/1061 ## [0.7.1] - 2025-04-02 diff --git a/crates/stackable-versioned/Cargo.toml b/crates/stackable-versioned/Cargo.toml index f25cc40d..1db42acc 100644 --- a/crates/stackable-versioned/Cargo.toml +++ b/crates/stackable-versioned/Cargo.toml @@ -31,3 +31,8 @@ serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } serde_yaml = { workspace = true, optional = true } snafu = { workspace = true, optional = true } + +[dev-dependencies] +insta.workspace = true +k8s-openapi.workspace = true +kube.workspace = true diff --git a/crates/stackable-versioned/src/k8s.rs b/crates/stackable-versioned/src/k8s.rs index 63dd01ad..f55182fa 100644 --- a/crates/stackable-versioned/src/k8s.rs +++ b/crates/stackable-versioned/src/k8s.rs @@ -47,17 +47,20 @@ fn raw_object_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema { /// This error indicates that parsing an object from a conversion review failed. #[derive(Debug, Snafu)] pub enum ParseObjectError { - #[snafu(display(r#"failed to find "apiVersion" field"#))] - FieldNotPresent, + #[snafu(display("the field {field:?} is missing"))] + FieldMissing { field: String }, - #[snafu(display(r#"the "apiVersion" field must be a string"#))] - FieldNotStr, + #[snafu(display("the field {field:?} must be a string"))] + FieldNotStr { field: String }, #[snafu(display("encountered unknown object API version {api_version:?}"))] UnknownApiVersion { api_version: String }, #[snafu(display("failed to deserialize object from JSON"))] Deserialize { source: serde_json::Error }, + + #[snafu(display("unexpected object kind {kind:?}, expected {expected:?}"))] + UnexpectedKind { kind: String, expected: String }, } /// This error indicates that converting an object from a conversion review to the desired @@ -69,6 +72,11 @@ pub enum ConvertObjectError { #[snafu(display("failed to serialize object into json"))] Serialize { source: serde_json::Error }, + + #[snafu(display("failed to parse desired API version"))] + ParseDesiredApiVersion { + source: UnknownDesiredApiVersionError, + }, } impl ConvertObjectError { @@ -89,6 +97,17 @@ impl ConvertObjectError { match self { ConvertObjectError::Parse { .. } => 400, ConvertObjectError::Serialize { .. } => 500, + + // This is likely the clients fault, as it is requesting a unsupported version + ConvertObjectError::ParseDesiredApiVersion { + source: UnknownDesiredApiVersionError { .. }, + } => 400, } } } + +#[derive(Debug, Snafu)] +#[snafu(display("unknown API version {api_version:?}"))] +pub struct UnknownDesiredApiVersionError { + pub api_version: String, +} diff --git a/crates/stackable-versioned/tests/conversions.rs b/crates/stackable-versioned/tests/conversions.rs new file mode 100644 index 00000000..03445400 --- /dev/null +++ b/crates/stackable-versioned/tests/conversions.rs @@ -0,0 +1,139 @@ +use std::{fs::File, path::Path}; + +use insta::{assert_snapshot, glob}; +use kube::{ + CustomResource, + core::{conversion::ConversionReview, response::StatusSummary}, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use stackable_versioned::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 From for String { + fn from(value: Gender) -> Self { + match value { + Gender::Unknown => "Unknown".to_owned(), + Gender::Male => "Male".to_owned(), + Gender::Female => "Female".to_owned(), + } + } +} + +impl From for Gender { + fn from(value: String) -> Self { + match value.as_str() { + "Male" => Self::Male, + "Female" => Self::Female, + _ => Self::Unknown, + } + } +} + +#[test] +fn pass() { + glob!("./inputs/conversions/pass/", "*.json", |path| { + let (request, response) = convert_via_file(path); + + let formatted = serde_json::to_string_pretty(&response) + .expect("Failed to serialize ConversionResponse"); + assert_snapshot!(formatted); + + let response = response + .response + .expect("ConversionReview had no response!"); + + assert_eq!( + response.result.status, + Some(StatusSummary::Success), + "File {path:?} should be converted successfully" + ); + assert_eq!(request.request.unwrap().uid, response.uid); + }) +} + +#[test] +fn fail() { + glob!("./inputs/conversions/fail/", "*.json", |path| { + let (request, response) = convert_via_file(path); + + let formatted = serde_json::to_string_pretty(&response) + .expect("Failed to serialize ConversionResponse"); + assert_snapshot!(formatted); + + let response = response + .response + .expect("ConversionReview had no response!"); + + assert_eq!( + response.result.status, + Some(StatusSummary::Failure), + "File {path:?} should *not* be converted successfully" + ); + if let Some(request) = &request.request { + assert_eq!(request.uid, response.uid); + } + }) +} + +fn convert_via_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::try_convert(request.clone()); + + (request, response) +} diff --git a/crates/stackable-versioned/tests/inputs/conversions/fail/request_missing.json b/crates/stackable-versioned/tests/inputs/conversions/fail/request_missing.json new file mode 100644 index 00000000..b5759bcd --- /dev/null +++ b/crates/stackable-versioned/tests/inputs/conversions/fail/request_missing.json @@ -0,0 +1,4 @@ +{ + "kind": "ConversionReview", + "apiVersion": "apiextensions.k8s.io/v1" +} diff --git a/crates/stackable-versioned/tests/inputs/conversions/fail/unkown_current_version.json b/crates/stackable-versioned/tests/inputs/conversions/fail/unkown_current_version.json new file mode 100644 index 00000000..6df3bb2d --- /dev/null +++ b/crates/stackable-versioned/tests/inputs/conversions/fail/unkown_current_version.json @@ -0,0 +1,22 @@ +{ + "kind": "ConversionReview", + "apiVersion": "apiextensions.k8s.io/v1", + "request": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "desiredAPIVersion": "test.stackable.tech/v3", + "objects": [ + { + "apiVersion": "test.stackable.tech/v99", + "kind": "Person", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": { + "username": "sbernauer" + } + } + ] + } +} diff --git a/crates/stackable-versioned/tests/inputs/conversions/fail/unkown_desired_version.json b/crates/stackable-versioned/tests/inputs/conversions/fail/unkown_desired_version.json new file mode 100644 index 00000000..736a95df --- /dev/null +++ b/crates/stackable-versioned/tests/inputs/conversions/fail/unkown_desired_version.json @@ -0,0 +1,22 @@ +{ + "kind": "ConversionReview", + "apiVersion": "apiextensions.k8s.io/v1", + "request": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "desiredAPIVersion": "test.stackable.tech/v99", + "objects": [ + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": { + "username": "sbernauer" + } + } + ] + } +} diff --git a/crates/stackable-versioned/tests/inputs/conversions/fail/unparseable_missing_field.json b/crates/stackable-versioned/tests/inputs/conversions/fail/unparseable_missing_field.json new file mode 100644 index 00000000..56b8c5c8 --- /dev/null +++ b/crates/stackable-versioned/tests/inputs/conversions/fail/unparseable_missing_field.json @@ -0,0 +1,20 @@ +{ + "kind": "ConversionReview", + "apiVersion": "apiextensions.k8s.io/v1", + "request": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "desiredAPIVersion": "test.stackable.tech/v3", + "objects": [ + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": {} + } + ] + } +} diff --git a/crates/stackable-versioned/tests/inputs/conversions/fail/wrong_kind.json b/crates/stackable-versioned/tests/inputs/conversions/fail/wrong_kind.json new file mode 100644 index 00000000..3c5bbcd6 --- /dev/null +++ b/crates/stackable-versioned/tests/inputs/conversions/fail/wrong_kind.json @@ -0,0 +1,22 @@ +{ + "kind": "ConversionReview", + "apiVersion": "apiextensions.k8s.io/v1", + "request": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "desiredAPIVersion": "test.stackable.tech/v3", + "objects": [ + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "SomeOtherResource", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": { + "username": "sbernauer" + } + } + ] + } +} diff --git a/crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_v1alpha1.json b/crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_v1alpha1.json new file mode 100644 index 00000000..2a052976 --- /dev/null +++ b/crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_v1alpha1.json @@ -0,0 +1,64 @@ +{ + "kind": "ConversionReview", + "apiVersion": "apiextensions.k8s.io/v1", + "request": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "desiredAPIVersion": "test.stackable.tech/v1alpha1", + "objects": [ + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": { + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v1alpha2", + "kind": "Person", + "metadata": {}, + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v1beta1", + "kind": "Person", + "metadata": {}, + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v2", + "kind": "Person", + "metadata": {}, + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer", + "gender": "Male" + } + }, + { + "apiVersion": "test.stackable.tech/v3", + "kind": "Person", + "metadata": {}, + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer", + "gender": "Male" + } + } + ] + } +} diff --git a/crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_v3.json b/crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_v3.json new file mode 100644 index 00000000..14848515 --- /dev/null +++ b/crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_v3.json @@ -0,0 +1,64 @@ +{ + "kind": "ConversionReview", + "apiVersion": "apiextensions.k8s.io/v1", + "request": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "desiredAPIVersion": "test.stackable.tech/v3", + "objects": [ + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": { + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v1alpha2", + "kind": "Person", + "metadata": {}, + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v1beta1", + "kind": "Person", + "metadata": {}, + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v2", + "kind": "Person", + "metadata": {}, + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer", + "gender": "Male" + } + }, + { + "apiVersion": "test.stackable.tech/v3", + "kind": "Person", + "metadata": {}, + "spec": { + "username": "sbernauer", + "firstName": "Sebastian", + "lastName": "Bernauer", + "gender": "Male" + } + } + ] + } +} diff --git a/crates/stackable-versioned/tests/snapshots/conversions__fail@request_missing.json.snap b/crates/stackable-versioned/tests/snapshots/conversions__fail@request_missing.json.snap new file mode 100644 index 00000000..1e4cf025 --- /dev/null +++ b/crates/stackable-versioned/tests/snapshots/conversions__fail@request_missing.json.snap @@ -0,0 +1,19 @@ +--- +source: crates/stackable-versioned/tests/conversions.rs +expression: formatted +input_file: crates/stackable-versioned/tests/inputs/conversions/fail/request_missing.json +--- +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "ConversionReview", + "response": { + "uid": "", + "result": { + "status": "Failure", + "code": 400, + "message": "request missing in ConversionReview", + "reason": "request missing in ConversionReview" + }, + "convertedObjects": [] + } +} diff --git a/crates/stackable-versioned/tests/snapshots/conversions__fail@unkown_current_version.json.snap b/crates/stackable-versioned/tests/snapshots/conversions__fail@unkown_current_version.json.snap new file mode 100644 index 00000000..5a79d017 --- /dev/null +++ b/crates/stackable-versioned/tests/snapshots/conversions__fail@unkown_current_version.json.snap @@ -0,0 +1,19 @@ +--- +source: crates/stackable-versioned/tests/conversions.rs +expression: formatted +input_file: crates/stackable-versioned/tests/inputs/conversions/fail/unkown_current_version.json +--- +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "ConversionReview", + "response": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "result": { + "status": "Failure", + "code": 400, + "message": "failed to parse object: encountered unknown object API version \"test.stackable.tech/v99\"", + "reason": "failed to parse object: encountered unknown object API version \"test.stackable.tech/v99\"" + }, + "convertedObjects": [] + } +} diff --git a/crates/stackable-versioned/tests/snapshots/conversions__fail@unkown_desired_version.json.snap b/crates/stackable-versioned/tests/snapshots/conversions__fail@unkown_desired_version.json.snap new file mode 100644 index 00000000..dc141268 --- /dev/null +++ b/crates/stackable-versioned/tests/snapshots/conversions__fail@unkown_desired_version.json.snap @@ -0,0 +1,19 @@ +--- +source: crates/stackable-versioned/tests/conversions.rs +expression: formatted +input_file: crates/stackable-versioned/tests/inputs/conversions/fail/unkown_desired_version.json +--- +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "ConversionReview", + "response": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "result": { + "status": "Failure", + "code": 400, + "message": "failed to parse desired API version: unknown API version \"test.stackable.tech/v99\"", + "reason": "failed to parse desired API version: unknown API version \"test.stackable.tech/v99\"" + }, + "convertedObjects": [] + } +} diff --git a/crates/stackable-versioned/tests/snapshots/conversions__fail@unparseable_missing_field.json.snap b/crates/stackable-versioned/tests/snapshots/conversions__fail@unparseable_missing_field.json.snap new file mode 100644 index 00000000..cb9ad38b --- /dev/null +++ b/crates/stackable-versioned/tests/snapshots/conversions__fail@unparseable_missing_field.json.snap @@ -0,0 +1,19 @@ +--- +source: crates/stackable-versioned/tests/conversions.rs +expression: formatted +input_file: crates/stackable-versioned/tests/inputs/conversions/fail/unparseable_missing_field.json +--- +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "ConversionReview", + "response": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "result": { + "status": "Failure", + "code": 400, + "message": "failed to parse object: failed to deserialize object from JSON: missing field `username`", + "reason": "failed to parse object: failed to deserialize object from JSON: missing field `username`" + }, + "convertedObjects": [] + } +} diff --git a/crates/stackable-versioned/tests/snapshots/conversions__fail@wrong_kind.json.snap b/crates/stackable-versioned/tests/snapshots/conversions__fail@wrong_kind.json.snap new file mode 100644 index 00000000..9986793c --- /dev/null +++ b/crates/stackable-versioned/tests/snapshots/conversions__fail@wrong_kind.json.snap @@ -0,0 +1,19 @@ +--- +source: crates/stackable-versioned/tests/conversions.rs +expression: formatted +input_file: crates/stackable-versioned/tests/inputs/conversions/fail/wrong_kind.json +--- +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "ConversionReview", + "response": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "result": { + "status": "Failure", + "code": 400, + "message": "failed to parse object: unexpected object kind \"SomeOtherResource\", expected \"Person\"", + "reason": "failed to parse object: unexpected object kind \"SomeOtherResource\", expected \"Person\"" + }, + "convertedObjects": [] + } +} diff --git a/crates/stackable-versioned/tests/snapshots/conversions__pass@persons_to_v1alpha1.json.snap b/crates/stackable-versioned/tests/snapshots/conversions__pass@persons_to_v1alpha1.json.snap new file mode 100644 index 00000000..ced5fb42 --- /dev/null +++ b/crates/stackable-versioned/tests/snapshots/conversions__pass@persons_to_v1alpha1.json.snap @@ -0,0 +1,61 @@ +--- +source: crates/stackable-versioned/tests/conversions.rs +expression: formatted +input_file: crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_v1alpha1.json +--- +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "ConversionReview", + "response": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "result": { + "status": "Success" + }, + "convertedObjects": [ + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": { + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "metadata": {}, + "spec": { + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "metadata": {}, + "spec": { + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "metadata": {}, + "spec": { + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v1alpha1", + "kind": "Person", + "metadata": {}, + "spec": { + "username": "sbernauer" + } + } + ] + } +} diff --git a/crates/stackable-versioned/tests/snapshots/conversions__pass@persons_to_v3.json.snap b/crates/stackable-versioned/tests/snapshots/conversions__pass@persons_to_v3.json.snap new file mode 100644 index 00000000..f6b9a7ae --- /dev/null +++ b/crates/stackable-versioned/tests/snapshots/conversions__pass@persons_to_v3.json.snap @@ -0,0 +1,76 @@ +--- +source: crates/stackable-versioned/tests/conversions.rs +expression: formatted +input_file: crates/stackable-versioned/tests/inputs/conversions/pass/persons_to_v3.json +--- +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "ConversionReview", + "response": { + "uid": "c4e55572-ee1f-4e94-9097-28936985d45f", + "result": { + "status": "Success" + }, + "convertedObjects": [ + { + "apiVersion": "test.stackable.tech/v3", + "kind": "Person", + "metadata": { + "name": "sbernauer", + "namespace": "default", + "uid": "d41e2019-5de3-409c-a7b2-0799ecb95e4b" + }, + "spec": { + "firstName": "", + "gender": "Unknown", + "lastName": "", + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v3", + "kind": "Person", + "metadata": {}, + "spec": { + "firstName": "Sebastian", + "gender": "Unknown", + "lastName": "Bernauer", + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v3", + "kind": "Person", + "metadata": {}, + "spec": { + "firstName": "Sebastian", + "gender": "Unknown", + "lastName": "Bernauer", + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v3", + "kind": "Person", + "metadata": {}, + "spec": { + "firstName": "Sebastian", + "gender": "Male", + "lastName": "Bernauer", + "username": "sbernauer" + } + }, + { + "apiVersion": "test.stackable.tech/v3", + "kind": "Person", + "metadata": {}, + "spec": { + "firstName": "Sebastian", + "gender": "Male", + "lastName": "Bernauer", + "username": "sbernauer" + } + } + ] + } +}