Skip to content

Commit 7d9bcce

Browse files
Danil GrigorevDanil Grigorev
authored andcommitted
Transform optional enums to match pre kube 2.0.0 format
1 parent a12559d commit 7d9bcce

File tree

3 files changed

+95
-2
lines changed

3 files changed

+95
-2
lines changed

kube-core/src/schema.rs

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
// Used in docs
66
#[allow(unused_imports)] use schemars::generate::SchemaSettings;
77

8-
use schemars::{transform::Transform, JsonSchema};
8+
use schemars::{
9+
transform::{transform_subschemas, Transform},
10+
JsonSchema,
11+
};
912
use serde::{Deserialize, Serialize};
10-
use serde_json::Value;
13+
use serde_json::{json, Value};
1114
use std::collections::{btree_map::Entry, BTreeMap, BTreeSet};
1215

1316
/// schemars [`Visitor`] that rewrites a [`Schema`] to conform to Kubernetes' "structural schema" rules
@@ -27,6 +30,42 @@ use std::collections::{btree_map::Entry, BTreeMap, BTreeSet};
2730
#[derive(Debug, Clone)]
2831
pub struct StructuralSchemaRewriter;
2932

33+
/// Recursively restructures JSON Schema objects so that the Option<Enum> object
34+
/// is returned per k8s CRD schema expectations.
35+
///
36+
/// In kube 2.x the schema output behavior for `Option<Enum>` types changed.
37+
///
38+
/// Previously given an enum like:
39+
///
40+
/// ```rust
41+
/// enum LogLevel {
42+
/// Debug,
43+
/// Info,
44+
/// Error,
45+
/// }
46+
/// ```
47+
///
48+
/// The following would be generated for Optional<LogLevel>:
49+
///
50+
/// ```json
51+
/// { "enum": ["Debug", "Info", "Error"], "type": "string", "nullable": true }
52+
/// ```
53+
///
54+
/// Now, schemars generates `anyOf` for `Option<LogLevel>` like:
55+
///
56+
/// ```json
57+
/// {
58+
/// "anyOf": [
59+
/// { "enum": ["Debug", "Info", "Error"], "type": "string" },
60+
/// { "enum": [null], "nullable": true }
61+
/// ]
62+
/// }
63+
/// ```
64+
///
65+
/// This transform implementation prevents this specific case from happening.
66+
#[derive(Debug, Clone, Default)]
67+
pub struct OptionalEnum;
68+
3069
/// A JSON Schema.
3170
#[allow(clippy::large_enum_variant)]
3271
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)]
@@ -305,6 +344,45 @@ impl Transform for StructuralSchemaRewriter {
305344
}
306345
}
307346

347+
impl Transform for OptionalEnum {
348+
fn transform(&mut self, schema: &mut schemars::Schema) {
349+
transform_subschemas(self, schema);
350+
351+
let obj = match schema.as_object_mut().filter(|o| o.len() == 1) {
352+
Some(obj) => obj,
353+
None => return,
354+
};
355+
356+
let arr = obj
357+
.get("anyOf")
358+
.iter()
359+
.flat_map(|any_of| any_of.as_array())
360+
.last()
361+
.cloned()
362+
.unwrap_or_default();
363+
364+
if arr.len() != 2 {
365+
return;
366+
}
367+
368+
let (first, second) = match (arr[0].as_object(), arr[1].as_object()) {
369+
(Some(first), Some(second)) => (first, second),
370+
_ => return,
371+
};
372+
373+
if first.contains_key("enum")
374+
&& second.contains_key("enum")
375+
&& second.get("enum") == Some(&json!([null]))
376+
&& !first.contains_key("nullable")
377+
&& second.contains_key("nullable")
378+
{
379+
obj.remove("anyOf");
380+
obj.append(&mut first.clone());
381+
obj.insert("nullable".to_string(), Value::Bool(true));
382+
}
383+
}
384+
}
385+
308386
/// Bring all plain enum values up to the root schema,
309387
/// since Kubernetes doesn't allow subschemas to define enum options.
310388
///

kube-derive/src/custom_resource.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,7 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea
677677
})
678678
.with_transform(#schemars::transform::AddNullable::default())
679679
.with_transform(#kube_core::schema::StructuralSchemaRewriter)
680+
.with_transform(#kube_core::schema::OptionalEnum)
680681
.into_generator();
681682
let schema = generate.into_root_schema_for::<Self>();
682683
}

kube-derive/tests/crd_schema_test.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use schemars::JsonSchema;
99
use serde::{Deserialize, Serialize};
1010
use std::collections::{HashMap, HashSet};
1111

12+
1213
// See `crd_derive_schema` example for how the schema generated from this struct affects defaulting and validation.
1314
#[derive(CustomResource, Serialize, Deserialize, Debug, PartialEq, Clone, KubeSchema)]
1415
#[kube(
@@ -77,6 +78,8 @@ struct FooSpec {
7778

7879
#[x_kube(merge_strategy = ListMerge::Set)]
7980
x_kubernetes_set: Vec<String>,
81+
82+
optional_enum: Option<Gender>,
8083
}
8184

8285
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)]
@@ -187,6 +190,7 @@ fn test_serialized_matches_expected() {
187190
my_list: vec!["".into()],
188191
set: HashSet::from(["foo".to_owned()]),
189192
x_kubernetes_set: vec![],
193+
optional_enum: Some(Gender::Other),
190194
}))
191195
.unwrap(),
192196
serde_json::json!({
@@ -222,6 +226,7 @@ fn test_serialized_matches_expected() {
222226
"myList": [""],
223227
"set": ["foo"],
224228
"xKubernetesSet": [],
229+
"optionalEnum": "Other",
225230
}
226231
})
227232
)
@@ -410,6 +415,15 @@ fn test_crd_schema_matches_expected() {
410415
},
411416
"x-kubernetes-list-type": "set",
412417
},
418+
"optionalEnum": {
419+
"nullable": true,
420+
"type": "string",
421+
"enum": [
422+
"Female",
423+
"Male",
424+
"Other"
425+
],
426+
}
413427
},
414428
"required": [
415429
"complexEnum",

0 commit comments

Comments
 (0)