Skip to content

Commit 7cbd8cd

Browse files
authored
Merge pull request #690 from teozkr/feature/derive-schema-finegrained-optout
Allow opting out of schema derivation per kind
2 parents a79335e + a2fe3c4 commit 7cbd8cd

File tree

7 files changed

+216
-59
lines changed

7 files changed

+216
-59
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ UNRELEASED
1414
- Changed variants of error enums in `kube::runtime`. Replaced `snafu` with `thiserror`.
1515
* BREAKING: Removed unused error variants in `kube::Error`: `Connection`, `RequestBuild`, `RequestSend`, `RequestParse`.
1616
* BREAKING: Removed unused error variant `kube::error::ConfigError::LoadConfigFile`
17+
* BREAKING: Replaced feature `kube-derive/schema` with attribute `#[kube(schema)]` - #690
18+
- If you currently disable default `kube-derive` default features to avoid automatic schema generation, add `#[kube(schema = "disabled")]` to your spec struct instead
19+
* BREAKING: Moved `CustomResource` derive crate overrides into subattribute `#[kube(crates(...))]` - #690
20+
- Replace `#[kube(kube_core = .., k8s_openapi = .., schema = .., serde = .., serde_json = ..)]` with `#[kube(crates(kube_core = .., k8s_openapi = .., schema = .., serde = .., serde_json = ..))]`
1721

1822
0.63.2 / 2021-10-28
1923
===================

examples/Cargo.toml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ license = "Apache-2.0"
1414
release = false
1515

1616
[features]
17-
default = ["native-tls", "schema", "kubederive", "ws", "latest", "runtime"]
18-
kubederive = ["kube/derive"] # by default import kube-derive with its default features
19-
schema = ["kube-derive/schema"] # crd_derive_no_schema shows how to opt out
17+
default = ["native-tls", "kubederive", "ws", "latest", "runtime"]
18+
kubederive = ["kube/derive"]
2019
native-tls = ["kube/client", "kube/native-tls"]
2120
rustls-tls = ["kube/client", "kube/rustls-tls"]
2221
runtime = ["kube/runtime"]
@@ -83,7 +82,11 @@ path = "crd_derive.rs"
8382
name = "crd_derive_schema"
8483
path = "crd_derive_schema.rs"
8584

86-
[[example]] # run this without --no-default-features --features="native-tls"
85+
[[example]]
86+
name = "crd_derive_custom_schema"
87+
path = "crd_derive_custom_schema.rs"
88+
89+
[[example]]
8790
name = "crd_derive_no_schema"
8891
path = "crd_derive_no_schema.rs"
8992

examples/crd_derive_custom_schema.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use kube::CustomResourceExt;
2+
use kube_derive::CustomResource;
3+
use schemars::{
4+
schema::{InstanceType, ObjectValidation, Schema, SchemaObject},
5+
JsonSchema,
6+
};
7+
use serde::{Deserialize, Serialize};
8+
9+
/// CustomResource with manually implemented `JsonSchema`
10+
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone)]
11+
#[kube(
12+
group = "clux.dev",
13+
version = "v1",
14+
kind = "Bar",
15+
namespaced,
16+
schema = "manual"
17+
)]
18+
pub struct MyBar {
19+
bars: u32,
20+
}
21+
22+
impl JsonSchema for Bar {
23+
fn schema_name() -> String {
24+
"Bar".to_string()
25+
}
26+
27+
fn json_schema(__gen: &mut schemars::gen::SchemaGenerator) -> Schema {
28+
Schema::Object(SchemaObject {
29+
object: Some(Box::new(ObjectValidation {
30+
required: ["spec".to_string()].into(),
31+
properties: [(
32+
"spec".to_string(),
33+
Schema::Object(SchemaObject {
34+
instance_type: Some(InstanceType::Object.into()),
35+
object: Some(Box::new(ObjectValidation {
36+
required: ["bars".to_string()].into(),
37+
properties: [(
38+
"bars".to_string(),
39+
Schema::Object(SchemaObject {
40+
instance_type: Some(InstanceType::Integer.into()),
41+
..SchemaObject::default()
42+
}),
43+
)]
44+
.into(),
45+
..ObjectValidation::default()
46+
})),
47+
..SchemaObject::default()
48+
}),
49+
)]
50+
.into(),
51+
..ObjectValidation::default()
52+
})),
53+
..SchemaObject::default()
54+
})
55+
}
56+
}
57+
58+
fn main() {
59+
let crd = Bar::crd();
60+
println!("{}", serde_yaml::to_string(&crd).unwrap());
61+
}
62+
63+
// Verify CustomResource derivable still
64+
#[test]
65+
fn verify_bar_is_a_custom_resource() {
66+
use kube::Resource;
67+
use static_assertions::assert_impl_all;
68+
69+
println!("Kind {}", Bar::kind(&()));
70+
let bar = Bar::new("five", MyBar { bars: 5 });
71+
println!("Spec: {:?}", bar.spec);
72+
assert_impl_all!(Bar: kube::Resource, JsonSchema);
73+
74+
let crd = Bar::crd();
75+
for v in crd.spec.versions {
76+
assert!(v.schema.unwrap().open_api_v3_schema.is_some());
77+
}
78+
}

examples/crd_derive_no_schema.rs

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
1-
#[cfg(not(feature = "schema"))]
21
use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::{
32
CustomResourceDefinition, CustomResourceValidation, JSONSchemaProps,
43
};
5-
#[cfg(not(feature = "schema"))] use kube_derive::CustomResource;
6-
#[cfg(not(feature = "schema"))] use serde::{Deserialize, Serialize};
4+
use kube_derive::CustomResource;
5+
use serde::{Deserialize, Serialize};
76

87
/// CustomResource with manually implemented schema
98
///
10-
/// NB: Everything here is gated on the example's `schema` feature not being set
11-
///
129
/// Normally you would do this by deriving JsonSchema or manually implementing it / parts of it.
1310
/// But here, we simply drop in a valid schema from a string and avoid schemars from the dependency tree entirely.
14-
#[cfg(not(feature = "schema"))]
1511
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone)]
16-
#[kube(group = "clux.dev", version = "v1", kind = "Bar", namespaced)]
12+
#[kube(
13+
group = "clux.dev",
14+
version = "v1",
15+
kind = "Bar",
16+
namespaced,
17+
schema = "disabled"
18+
)]
1719
pub struct MyBar {
1820
bars: u32,
1921
}
2022

21-
#[cfg(not(feature = "schema"))]
22-
const MANUAL_SCHEMA: &'static str = r#"
23+
const MANUAL_SCHEMA: &str = r#"
2324
type: object
2425
properties:
2526
spec:
@@ -31,7 +32,6 @@ properties:
3132
- bars
3233
"#;
3334

34-
#[cfg(not(feature = "schema"))]
3535
impl Bar {
3636
fn crd_with_manual_schema() -> CustomResourceDefinition {
3737
use kube::CustomResourceExt;
@@ -47,18 +47,12 @@ impl Bar {
4747
}
4848
}
4949

50-
#[cfg(not(feature = "schema"))]
5150
fn main() {
5251
let crd = Bar::crd_with_manual_schema();
5352
println!("{}", serde_yaml::to_string(&crd).unwrap());
5453
}
55-
#[cfg(feature = "schema")]
56-
fn main() {
57-
eprintln!("This example it disabled when using the schema feature");
58-
}
5954

6055
// Verify CustomResource derivable still
61-
#[cfg(not(feature = "schema"))]
6256
#[test]
6357
fn verify_bar_is_a_custom_resource() {
6458
use kube::Resource;

kube-derive/Cargo.toml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@ darling = "0.13.0"
2222
[lib]
2323
proc-macro = true
2424

25-
[features]
26-
default = ["schema"]
27-
schema = []
28-
2925
[dev-dependencies]
3026
serde = { version = "1.0.130", features = ["derive"] }
3127
serde_yaml = "0.8.21"

kube-derive/src/custom_resource.rs

Lines changed: 95 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use darling::FromDeriveInput;
1+
use darling::{FromDeriveInput, FromMeta};
22
use proc_macro2::{Ident, Span, TokenStream};
33
use syn::{parse_quote, Data, DeriveInput, Path, Visibility};
44

@@ -24,6 +24,8 @@ struct KubeAttrs {
2424
#[darling(multiple, rename = "derive")]
2525
derives: Vec<String>,
2626
#[darling(default)]
27+
schema: Option<SchemaMode>,
28+
#[darling(default)]
2729
status: Option<String>,
2830
#[darling(multiple, rename = "category")]
2931
categories: Vec<String>,
@@ -33,35 +35,92 @@ struct KubeAttrs {
3335
printcolums: Vec<String>,
3436
#[darling(default)]
3537
scale: Option<String>,
36-
#[darling(default = "default_kube_core")]
38+
#[darling(default)]
39+
crates: Crates,
40+
}
41+
42+
#[derive(Debug, FromMeta)]
43+
struct Crates {
44+
#[darling(default = "Self::default_kube_core")]
3745
kube_core: Path,
38-
#[darling(default = "default_k8s_openapi")]
46+
#[darling(default = "Self::default_k8s_openapi")]
3947
k8s_openapi: Path,
40-
#[darling(default = "default_schemars")]
48+
#[darling(default = "Self::default_schemars")]
4149
schemars: Path,
42-
#[darling(default = "default_serde")]
50+
#[darling(default = "Self::default_serde")]
4351
serde: Path,
44-
#[darling(default = "default_serde_json")]
52+
#[darling(default = "Self::default_serde_json")]
4553
serde_json: Path,
4654
}
4755

48-
fn default_apiext() -> String {
49-
"v1".to_owned()
56+
// Default is required when the subattribute isn't mentioned at all
57+
// Delegate to darling rather than deriving, so that we can piggyback off the `#[darling(default)]` clauses
58+
impl Default for Crates {
59+
fn default() -> Self {
60+
Self::from_list(&[]).unwrap()
61+
}
5062
}
51-
fn default_kube_core() -> Path {
52-
parse_quote! { ::kube::core } // by default must work well with people using facade crate
63+
64+
impl Crates {
65+
fn default_kube_core() -> Path {
66+
parse_quote! { ::kube::core } // by default must work well with people using facade crate
67+
}
68+
69+
fn default_k8s_openapi() -> Path {
70+
parse_quote! { ::k8s_openapi }
71+
}
72+
73+
fn default_schemars() -> Path {
74+
parse_quote! { ::schemars }
75+
}
76+
77+
fn default_serde() -> Path {
78+
parse_quote! { ::serde }
79+
}
80+
81+
fn default_serde_json() -> Path {
82+
parse_quote! { ::serde_json }
83+
}
5384
}
54-
fn default_k8s_openapi() -> Path {
55-
parse_quote! { ::k8s_openapi }
85+
86+
fn default_apiext() -> String {
87+
"v1".to_owned()
5688
}
57-
fn default_schemars() -> Path {
58-
parse_quote! { ::schemars }
89+
90+
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
91+
enum SchemaMode {
92+
Disabled,
93+
Manual,
94+
Derived,
5995
}
60-
fn default_serde() -> Path {
61-
parse_quote! { ::serde }
96+
97+
impl SchemaMode {
98+
fn derive(self) -> bool {
99+
match self {
100+
SchemaMode::Disabled => false,
101+
SchemaMode::Manual => false,
102+
SchemaMode::Derived => true,
103+
}
104+
}
105+
106+
fn use_in_crd(self) -> bool {
107+
match self {
108+
SchemaMode::Disabled => false,
109+
SchemaMode::Manual => true,
110+
SchemaMode::Derived => true,
111+
}
112+
}
62113
}
63-
fn default_serde_json() -> Path {
64-
parse_quote! { ::serde_json }
114+
115+
impl FromMeta for SchemaMode {
116+
fn from_string(value: &str) -> darling::Result<Self> {
117+
match value {
118+
"disabled" => Ok(SchemaMode::Disabled),
119+
"manual" => Ok(SchemaMode::Manual),
120+
"derived" => Ok(SchemaMode::Derived),
121+
x => Err(darling::Error::unknown_value(x)),
122+
}
123+
}
65124
}
66125

67126
pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
@@ -92,6 +151,7 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea
92151
version,
93152
namespaced,
94153
derives,
154+
schema: schema_mode,
95155
status,
96156
plural,
97157
singular,
@@ -100,11 +160,14 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea
100160
printcolums,
101161
apiextensions,
102162
scale,
103-
kube_core,
104-
k8s_openapi,
105-
schemars,
106-
serde,
107-
serde_json,
163+
crates:
164+
Crates {
165+
kube_core,
166+
k8s_openapi,
167+
schemars,
168+
serde,
169+
serde_json,
170+
},
108171
} = kube_attrs;
109172

110173
let struct_name = kind_struct.unwrap_or_else(|| kind.clone());
@@ -152,18 +215,21 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea
152215
}
153216
}
154217

155-
// Schema generation is always enabled for v1 because it's mandatory.
156-
// TODO Enable schema generation for v1beta1 if the spec derives `JsonSchema`.
157-
let schema_gen_enabled = apiextensions == "v1" && cfg!(feature = "schema");
218+
// Enable schema generation by default for v1 because it's mandatory.
219+
let schema_mode = schema_mode.unwrap_or(if apiextensions == "v1" {
220+
SchemaMode::Derived
221+
} else {
222+
SchemaMode::Disabled
223+
});
158224
// We exclude fields `apiVersion`, `kind`, and `metadata` from our schema because
159225
// these are validated by the API server implicitly. Also, we can't generate the
160226
// schema for `metadata` (`ObjectMeta`) because it doesn't implement `JsonSchema`.
161-
let schemars_skip = if schema_gen_enabled {
227+
let schemars_skip = if schema_mode.derive() {
162228
quote! { #[schemars(skip)] }
163229
} else {
164230
quote! {}
165231
};
166-
if schema_gen_enabled {
232+
if schema_mode.derive() {
167233
derive_paths.push(syn::parse_quote! { #schemars::JsonSchema });
168234
}
169235

@@ -289,7 +355,7 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea
289355
let crd_meta_name = format!("{}.{}", plural, group);
290356
let crd_meta = quote! { { "name": #crd_meta_name } };
291357

292-
let schemagen = if schema_gen_enabled {
358+
let schemagen = if schema_mode.use_in_crd() {
293359
quote! {
294360
// Don't use definitions and don't include `$schema` because these are not allowed.
295361
let gen = #schemars::gen::SchemaSettings::openapi3().with(|s| {

0 commit comments

Comments
 (0)