Skip to content

Commit cccc052

Browse files
committed
test: Add schema test, fix FromMeta implementation
Adding this test proved to be very valuable because the FromMeta implemenetation had a few errors and resulted in different panic messages coming from the derive macro. I also added a small note to the #[kube(scale(...))] section stating that the scale subresource can only be used when the status subresource is used as well. I plan to further improve the validation in a future pull request. Signed-off-by: Techassi <[email protected]>
1 parent 7a86117 commit cccc052

File tree

3 files changed

+90
-33
lines changed

3 files changed

+90
-33
lines changed

kube-derive/src/custom_resource.rs

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -235,18 +235,36 @@ impl FromMeta for Scale {
235235

236236
match name.as_str() {
237237
"label_selector_path" => {
238-
let path = errors.handle(darling::FromMeta::from_meta(meta));
239-
label_selector_path = (true, Some(path))
238+
if !label_selector_path.0 {
239+
let path = errors.handle(darling::FromMeta::from_meta(meta));
240+
label_selector_path = (true, Some(path))
241+
} else {
242+
errors.push(
243+
darling::Error::duplicate_field("label_selector_path").with_span(&meta),
244+
);
245+
}
240246
}
241247
"spec_replicas_path" => {
242-
let path = errors.handle(darling::FromMeta::from_meta(meta));
243-
spec_replicas_path = (true, path)
248+
if !spec_replicas_path.0 {
249+
let path = errors.handle(darling::FromMeta::from_meta(meta));
250+
spec_replicas_path = (true, path)
251+
} else {
252+
errors.push(
253+
darling::Error::duplicate_field("spec_replicas_path").with_span(&meta),
254+
);
255+
}
244256
}
245257
"status_replicas_path" => {
246-
let path = errors.handle(darling::FromMeta::from_meta(meta));
247-
status_replicas_path = (true, path)
258+
if !status_replicas_path.0 {
259+
let path = errors.handle(darling::FromMeta::from_meta(meta));
260+
status_replicas_path = (true, path)
261+
} else {
262+
errors.push(
263+
darling::Error::duplicate_field("status_replicas_path").with_span(&meta),
264+
);
265+
}
248266
}
249-
other => return Err(darling::Error::unknown_field(other)),
267+
other => errors.push(darling::Error::unknown_field(other)),
250268
}
251269
}
252270
darling::ast::NestedMeta::Lit(lit) => {
@@ -255,13 +273,6 @@ impl FromMeta for Scale {
255273
}
256274
}
257275

258-
if !label_selector_path.0 {
259-
match <Option<String> as darling::FromMeta>::from_none() {
260-
Some(fallback) => label_selector_path.1 = Some(fallback),
261-
None => errors.push(darling::Error::missing_field("spec_replicas_path")),
262-
}
263-
}
264-
265276
if !spec_replicas_path.0 && spec_replicas_path.1.is_none() {
266277
errors.push(darling::Error::missing_field("spec_replicas_path"));
267278
}
@@ -270,8 +281,10 @@ impl FromMeta for Scale {
270281
errors.push(darling::Error::missing_field("status_replicas_path"));
271282
}
272283

273-
errors.finish_with(Self {
274-
label_selector_path: label_selector_path.1.unwrap(),
284+
errors.finish()?;
285+
286+
Ok(Self {
287+
label_selector_path: label_selector_path.1.unwrap_or_default(),
275288
spec_replicas_path: spec_replicas_path.1.unwrap(),
276289
status_replicas_path: status_replicas_path.1.unwrap(),
277290
})
@@ -287,7 +300,7 @@ impl Scale {
287300
let label_selector_path = self
288301
.label_selector_path
289302
.as_ref()
290-
.map_or_else(|| quote! { None }, |p| quote! { #p.into() });
303+
.map_or_else(|| quote! { None }, |p| quote! { Some(#p.into()) });
291304
let spec_replicas_path = &self.spec_replicas_path;
292305
let status_replicas_path = &self.status_replicas_path;
293306

kube-derive/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ mod resource;
132132
/// ## `#[kube(scale(...))]`
133133
///
134134
/// Allow customizing the scale struct for the [scale subresource](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#subresources).
135+
/// It should be noted, that the status subresource must also be enabled to use the scale subresource. This is because
136+
/// the `statusReplicasPath` only accepts JSONPaths under `.status`.
135137
///
136138
/// ```ignore
137139
/// #[kube(scale(

kube-derive/tests/crd_schema_test.rs

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#![allow(missing_docs)]
12
#![recursion_limit = "256"]
23

34
use assert_json_diff::assert_json_eq;
@@ -29,6 +30,12 @@ use std::collections::{HashMap, HashSet};
2930
label("clux.dev", "cluxingv1"),
3031
label("clux.dev/persistence", "disabled"),
3132
rule = Rule::new("self.metadata.name == 'singleton'"),
33+
status = "Status",
34+
scale(
35+
spec_replicas_path = ".spec.replicas",
36+
status_replicas_path = ".status.replicas",
37+
label_selector_path = ".status.labelSelector"
38+
),
3239
)]
3340
#[cel_validate(rule = Rule::new("has(self.nonNullable)"))]
3441
#[serde(rename_all = "camelCase")]
@@ -62,6 +69,13 @@ struct FooSpec {
6269
set: HashSet<String>,
6370
}
6471

72+
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)]
73+
#[serde(rename_all = "camelCase")]
74+
pub struct Status {
75+
replicas: usize,
76+
label_selector: String,
77+
}
78+
6579
fn default_value() -> String {
6680
"default_value".into()
6781
}
@@ -140,21 +154,24 @@ fn test_shortnames() {
140154
#[test]
141155
fn test_serialized_matches_expected() {
142156
assert_json_eq!(
143-
serde_json::to_value(Foo::new("bar", FooSpec {
144-
non_nullable: "asdf".to_string(),
145-
non_nullable_with_default: "asdf".to_string(),
146-
nullable_skipped: None,
147-
nullable: None,
148-
nullable_skipped_with_default: None,
149-
nullable_with_default: None,
150-
timestamp: DateTime::from_timestamp(0, 0).unwrap(),
151-
complex_enum: ComplexEnum::VariantOne { int: 23 },
152-
untagged_enum_person: UntaggedEnumPerson::GenderAndAge(GenderAndAge {
153-
age: 42,
154-
gender: Gender::Male,
155-
}),
156-
set: HashSet::from(["foo".to_owned()])
157-
}))
157+
serde_json::to_value(Foo::new(
158+
"bar",
159+
FooSpec {
160+
non_nullable: "asdf".to_string(),
161+
non_nullable_with_default: "asdf".to_string(),
162+
nullable_skipped: None,
163+
nullable: None,
164+
nullable_skipped_with_default: None,
165+
nullable_with_default: None,
166+
timestamp: DateTime::from_timestamp(0, 0).unwrap(),
167+
complex_enum: ComplexEnum::VariantOne { int: 23 },
168+
untagged_enum_person: UntaggedEnumPerson::GenderAndAge(GenderAndAge {
169+
age: 42,
170+
gender: Gender::Male,
171+
}),
172+
set: HashSet::from(["foo".to_owned()])
173+
}
174+
))
158175
.unwrap(),
159176
serde_json::json!({
160177
"apiVersion": "clux.dev/v1",
@@ -231,6 +248,14 @@ fn test_crd_schema_matches_expected() {
231248
}, {
232249
"jsonPath": ".spec.nullable"
233250
}],
251+
"subresources": {
252+
"status": {},
253+
"scale": {
254+
"specReplicasPath": ".spec.replicas",
255+
"labelSelectorPath": ".status.labelSelector",
256+
"statusReplicasPath": ".status.replicas"
257+
}
258+
},
234259
"schema": {
235260
"openAPIV3Schema": {
236261
"description": "Custom resource representing a Foo",
@@ -358,6 +383,24 @@ fn test_crd_schema_matches_expected() {
358383
"rule": "has(self.nonNullable)",
359384
}],
360385
"type": "object"
386+
},
387+
"status": {
388+
"properties": {
389+
"replicas": {
390+
"type": "integer",
391+
"format": "uint",
392+
"minimum": 0.0,
393+
},
394+
"labelSelector": {
395+
"type": "string"
396+
}
397+
},
398+
"required": [
399+
"labelSelector",
400+
"replicas"
401+
],
402+
"nullable": true,
403+
"type": "object"
361404
}
362405
},
363406
"required": [
@@ -370,7 +413,6 @@ fn test_crd_schema_matches_expected() {
370413
"type": "object"
371414
}
372415
},
373-
"subresources": {},
374416
}
375417
]
376418
}

0 commit comments

Comments
 (0)