Skip to content

Commit 7d83ffe

Browse files
sbernauerNickLarsenNZTechassi
authored
fix(stackable-versioned): Validate kind, desired and current version (#1061)
* fix: ConversionReview not working correctly. Also added some tests * formatting * Move tests into dedicated folder * move fixtures * Check Kind of send object * Add comment * changelog * clippy * Move kind check to from_json_value * Parse desired api version early * Remove leftover comment * Update crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs Co-authored-by: Nick <[email protected]> * Update crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs Co-authored-by: Techassi <[email protected]> * Avoid stringify! * adopt unexpected kind error message * Add ParseApiVersionError variant * Update crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs Co-authored-by: Techassi <[email protected]> * Apply suggestions from code review Co-authored-by: Techassi <[email protected]> * Update crates/stackable-versioned/src/k8s.rs Co-authored-by: Techassi <[email protected]> * Move into tests folder * to_string -> to_owned * Rename fn to try_from_json_object * Remove try_prefix * Move test inputs folder * Rename run_for_file -> convert_via_file * Derive copy, clone and debug * try_from_json_object -> from_json_object * conversion.rs -> conversions.rs * simplify noop match * Simply noop branch * YOLO * Add changelog entry * changelog --------- Co-authored-by: Nick <[email protected]> Co-authored-by: Techassi <[email protected]>
1 parent 045a8cc commit 7d83ffe

27 files changed

+1295
-224
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/stackable-versioned-macros/src/codegen/container/struct/k8s.rs

Lines changed: 82 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ impl Struct {
129129
let variant_data_ident = &self.common.idents.kubernetes_parameter;
130130
let version_enum_ident = &self.common.idents.kubernetes_version;
131131
let enum_ident = &self.common.idents.kubernetes;
132+
let enum_ident_string = enum_ident.to_string();
132133

133134
// Only add the #[automatically_derived] attribute if this impl is used outside of a
134135
// module (in standalone mode).
@@ -153,7 +154,8 @@ impl Struct {
153154
// Generate additional Kubernetes code, this is split out to reduce the complexity in this
154155
// function.
155156
let status_struct = self.generate_kubernetes_status_struct(kubernetes_arguments, is_nested);
156-
let version_enum = self.generate_kubernetes_version_enum(tokens, vis, is_nested);
157+
let version_enum =
158+
self.generate_kubernetes_version_enum(kubernetes_arguments, tokens, vis, is_nested);
157159
let convert_method = self.generate_kubernetes_conversion(versions);
158160

159161
let parse_object_error = quote! { #versioned_path::ParseObjectError };
@@ -173,21 +175,39 @@ impl Struct {
173175
#k8s_openapi_path::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition,
174176
#kube_core_path::crd::MergeError>
175177
{
176-
#kube_core_path::crd::merge_crds(vec![#(#crd_fns),*], stored_apiversion.as_str())
178+
#kube_core_path::crd::merge_crds(vec![#(#crd_fns),*], stored_apiversion.as_version_str())
177179
}
178180

179181
#convert_method
180182

181-
fn from_json_value(value: #serde_json_path::Value) -> ::std::result::Result<Self, #parse_object_error> {
182-
let api_version = value
183+
fn from_json_object(object_value: #serde_json_path::Value) -> ::std::result::Result<Self, #parse_object_error> {
184+
let object_kind = object_value
185+
.get("kind")
186+
.ok_or_else(|| #parse_object_error::FieldMissing{ field: "kind".to_owned() })?
187+
.as_str()
188+
.ok_or_else(|| #parse_object_error::FieldNotStr{ field: "kind".to_owned() })?;
189+
190+
// Note(@sbernauer): The kind must be checked here, because it is
191+
// possible for the wrong object to be deserialized.
192+
// Checking here stops us assuming the kind is correct and
193+
// accidentally updating upgrade/downgrade information in the
194+
// status in a later step.
195+
if object_kind != #enum_ident_string {
196+
return Err(#parse_object_error::UnexpectedKind{
197+
kind: object_kind.to_owned(),
198+
expected: #enum_ident_string.to_owned(),
199+
});
200+
}
201+
202+
let api_version = object_value
183203
.get("apiVersion")
184-
.ok_or_else(|| #parse_object_error::FieldNotPresent)?
204+
.ok_or_else(|| #parse_object_error::FieldMissing{ field: "apiVersion".to_owned() })?
185205
.as_str()
186-
.ok_or_else(|| #parse_object_error::FieldNotStr)?;
206+
.ok_or_else(|| #parse_object_error::FieldNotStr{ field: "apiVersion".to_owned() })?;
187207

188208
let object = match api_version {
189-
#(#api_versions | #variant_strings => {
190-
let object = #serde_json_path::from_value(value)
209+
#(#api_versions => {
210+
let object = #serde_json_path::from_value(object_value)
191211
.map_err(|source| #parse_object_error::Deserialize { source })?;
192212

193213
Self::#variant_idents(object)
@@ -218,6 +238,7 @@ impl Struct {
218238

219239
fn generate_kubernetes_version_enum(
220240
&self,
241+
kubernetes_arguments: &KubernetesArguments,
221242
tokens: &KubernetesTokens,
222243
vis: &Visibility,
223244
is_nested: bool,
@@ -228,30 +249,55 @@ impl Struct {
228249
// module (in standalone mode).
229250
let automatically_derived = is_nested.not().then(|| quote! {#[automatically_derived]});
230251

252+
let versioned_path = &*kubernetes_arguments.crates.versioned;
253+
let unknown_desired_api_version_error =
254+
quote! { #versioned_path::UnknownDesiredApiVersionError };
255+
231256
// Get the per-version items to be able to iterate over them via quote
232257
let variant_strings = &tokens.variant_strings;
233258
let variant_idents = &tokens.variant_idents;
259+
let api_versions = variant_strings
260+
.iter()
261+
.map(|version| format!("{group}/{version}", group = &kubernetes_arguments.group))
262+
.collect::<Vec<_>>();
234263

235264
quote! {
236265
#automatically_derived
266+
#[derive(Copy, Clone, Debug)]
237267
#vis enum #enum_ident {
238268
#(#variant_idents),*
239269
}
240270

241271
#automatically_derived
242272
impl ::std::fmt::Display for #enum_ident {
243273
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> {
244-
f.write_str(self.as_str())
274+
// The version (without the Kubernetes group) is probably more human-readable
275+
f.write_str(self.as_version_str())
245276
}
246277
}
247278

248279
#automatically_derived
249280
impl #enum_ident {
250-
pub fn as_str(&self) -> &str {
281+
pub fn as_version_str(&self) -> &str {
251282
match self {
252283
#(#variant_idents => #variant_strings),*
253284
}
254285
}
286+
287+
pub fn as_api_version_str(&self) -> &str {
288+
match self {
289+
#(#variant_idents => #api_versions),*
290+
}
291+
}
292+
293+
pub fn from_api_version(api_version: &str) -> Result<Self, #unknown_desired_api_version_error> {
294+
match api_version {
295+
#(#api_versions => Ok(Self::#variant_idents)),*,
296+
_ => Err(#unknown_desired_api_version_error {
297+
api_version: api_version.to_owned(),
298+
}),
299+
}
300+
}
255301
}
256302
}
257303
}
@@ -315,6 +361,7 @@ impl Struct {
315361
let kubernetes_arguments = self.common.options.kubernetes_arguments.as_ref()?;
316362

317363
let struct_ident = &self.common.idents.kubernetes;
364+
let version_enum_ident = &self.common.idents.kubernetes_version;
318365

319366
let kube_client_path = &*kubernetes_arguments.crates.kube_client;
320367
let serde_json_path = &*kubernetes_arguments.crates.serde_json;
@@ -324,7 +371,7 @@ impl Struct {
324371
let convert_object_error = quote! { #versioned_path::ConvertObjectError };
325372

326373
// Generate conversion paths and the match arms for these paths
327-
let match_arms =
374+
let conversion_match_arms =
328375
self.generate_kubernetes_conversion_match_arms(versions, kubernetes_arguments);
329376

330377
// TODO (@Techassi): Make this a feature, drop the option from the macro arguments
@@ -375,11 +422,8 @@ impl Struct {
375422
}
376423
};
377424

378-
// Extract the desired api version
379-
let desired_api_version = request.desired_api_version.as_str();
380-
381425
// Convert all objects into the desired version
382-
let response = match Self::convert_objects(request.objects, desired_api_version) {
426+
let response = match Self::convert_objects(request.objects, &request.desired_api_version) {
383427
::std::result::Result::Ok(converted_objects) => {
384428
#successful_conversion_response_event
385429

@@ -426,20 +470,31 @@ impl Struct {
426470
)
427471
-> ::std::result::Result<::std::vec::Vec<#serde_json_path::Value>, #convert_object_error>
428472
{
473+
let desired_api_version = #version_enum_ident::from_api_version(desired_api_version)
474+
.map_err(|source| #convert_object_error::ParseDesiredApiVersion { source })?;
475+
429476
let mut converted_objects = ::std::vec::Vec::with_capacity(objects.len());
430477

431478
for object in objects {
432479
// This clone is required because in the noop case we move the object into
433480
// the converted objects vec.
434-
let current_object = Self::from_json_value(object.clone())
481+
let current_object = Self::from_json_object(object.clone())
435482
.map_err(|source| #convert_object_error::Parse { source })?;
436483

437484
match (current_object, desired_api_version) {
438-
#(#match_arms,)*
439-
// If no match arm matches, this is a noop. This is the case if the desired
440-
// version matches the current object api version.
485+
#(#conversion_match_arms,)*
486+
// In case the desired version matches the current object api version, we
487+
// don't need to do anything.
488+
//
441489
// NOTE (@Techassi): I'm curious if this will ever happen? In theory the K8s
442490
// apiserver should never send such a conversion review.
491+
//
492+
// Note(@sbernauer): I would prefer to explicitly list the remaining no-op
493+
// cases, so the compiler ensures we did not miss a conversion
494+
// // let version_idents = versions.iter().map(|v| &v.idents.variant);
495+
// #(
496+
// (Self::#version_idents(_), #version_enum_ident::#version_idents)
497+
// )|* => converted_objects.push(object)
443498
_ => converted_objects.push(object),
444499
}
445500
}
@@ -454,8 +509,10 @@ impl Struct {
454509
versions: &[VersionDefinition],
455510
kubernetes_arguments: &KubernetesArguments,
456511
) -> Vec<TokenStream> {
512+
let group = &kubernetes_arguments.group;
457513
let variant_data_ident = &self.common.idents.kubernetes_parameter;
458514
let struct_ident = &self.common.idents.kubernetes;
515+
let version_enum_ident = &self.common.idents.kubernetes_version;
459516
let spec_ident = &self.common.idents.original;
460517

461518
let versioned_path = &*kubernetes_arguments.crates.versioned;
@@ -470,7 +527,10 @@ impl Struct {
470527
let current_object_version_string = &start.inner.to_string();
471528

472529
let desired_object_version = path.last().expect("the path always contains at least one element");
473-
let desired_object_version_string = desired_object_version.inner.to_string();
530+
let desired_object_api_version_string = format!(
531+
"{group}/{desired_object_version}",
532+
desired_object_version = desired_object_version.inner
533+
);
474534
let desired_object_variant_ident = &desired_object_version.idents.variant;
475535
let desired_object_module_ident = &desired_object_version.idents.module;
476536

@@ -493,7 +553,7 @@ impl Struct {
493553

494554
let convert_object_trace = kubernetes_arguments.options.enable_tracing.is_present().then(|| quote! {
495555
::tracing::trace!(
496-
#DESIRED_API_VERSION_ATTRIBUTE = #desired_object_version_string,
556+
#DESIRED_API_VERSION_ATTRIBUTE = #desired_object_api_version_string,
497557
#API_VERSION_ATTRIBUTE = #current_object_version_string,
498558
#STEPS_ATTRIBUTE = #steps,
499559
#KIND_ATTRIBUTE = #kind,
@@ -507,7 +567,7 @@ impl Struct {
507567
.then(|| quote! { status: #variant_data_ident.status, });
508568

509569
quote! {
510-
(Self::#current_object_version_ident(#variant_data_ident), #desired_object_version_string) => {
570+
(Self::#current_object_version_ident(#variant_data_ident), #version_enum_ident::#desired_object_variant_ident) => {
511571
#(#conversions)*
512572

513573
let desired_object = Self::#desired_object_variant_ident(#desired_object_module_ident::#struct_ident {

0 commit comments

Comments
 (0)