Skip to content

Allow opting out of schema derivation per kind #690

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ UNRELEASED
- Changed variants of error enums in `kube::runtime`. Replaced `snafu` with `thiserror`.
* BREAKING: Removed unused error variants in `kube::Error`: `Connection`, `RequestBuild`, `RequestSend`, `RequestParse`.
* BREAKING: Removed unused error variant `kube::error::ConfigError::LoadConfigFile`
* BREAKING: Replaced feature `kube-derive/schema` with attribute `#[kube(schema)]` - #690
- If you currently disable default `kube-derive` default features to avoid automatic schema generation, add `#[kube(schema = "disabled")]` to your spec struct instead
* BREAKING: Moved `CustomResource` derive crate overrides into subattribute `#[kube(crates(...))]` - #690
- Replace `#[kube(kube_core = .., k8s_openapi = .., schema = .., serde = .., serde_json = ..)]` with `#[kube(crates(kube_core = .., k8s_openapi = .., schema = .., serde = .., serde_json = ..))]`

0.63.2 / 2021-10-28
===================
Expand Down
11 changes: 7 additions & 4 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ license = "Apache-2.0"
release = false

[features]
default = ["native-tls", "schema", "kubederive", "ws", "latest", "runtime"]
kubederive = ["kube/derive"] # by default import kube-derive with its default features
schema = ["kube-derive/schema"] # crd_derive_no_schema shows how to opt out
default = ["native-tls", "kubederive", "ws", "latest", "runtime"]
kubederive = ["kube/derive"]
native-tls = ["kube/client", "kube/native-tls"]
rustls-tls = ["kube/client", "kube/rustls-tls"]
runtime = ["kube/runtime"]
Expand Down Expand Up @@ -83,7 +82,11 @@ path = "crd_derive.rs"
name = "crd_derive_schema"
path = "crd_derive_schema.rs"

[[example]] # run this without --no-default-features --features="native-tls"
[[example]]
name = "crd_derive_custom_schema"
path = "crd_derive_custom_schema.rs"

[[example]]
name = "crd_derive_no_schema"
path = "crd_derive_no_schema.rs"

Expand Down
78 changes: 78 additions & 0 deletions examples/crd_derive_custom_schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use kube::CustomResourceExt;
use kube_derive::CustomResource;
use schemars::{
schema::{InstanceType, ObjectValidation, Schema, SchemaObject},
JsonSchema,
};
use serde::{Deserialize, Serialize};

/// CustomResource with manually implemented `JsonSchema`
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone)]
#[kube(
group = "clux.dev",
version = "v1",
kind = "Bar",
namespaced,
schema = "manual"
)]
pub struct MyBar {
bars: u32,
}

impl JsonSchema for Bar {
fn schema_name() -> String {
"Bar".to_string()
}

fn json_schema(__gen: &mut schemars::gen::SchemaGenerator) -> Schema {
Schema::Object(SchemaObject {
object: Some(Box::new(ObjectValidation {
required: ["spec".to_string()].into(),
properties: [(
"spec".to_string(),
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::Object.into()),
object: Some(Box::new(ObjectValidation {
required: ["bars".to_string()].into(),
properties: [(
"bars".to_string(),
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::Integer.into()),
..SchemaObject::default()
}),
)]
.into(),
..ObjectValidation::default()
})),
..SchemaObject::default()
}),
)]
.into(),
..ObjectValidation::default()
})),
..SchemaObject::default()
})
}
}

fn main() {
let crd = Bar::crd();
println!("{}", serde_yaml::to_string(&crd).unwrap());
}

// Verify CustomResource derivable still
#[test]
fn verify_bar_is_a_custom_resource() {
use kube::Resource;
use static_assertions::assert_impl_all;

println!("Kind {}", Bar::kind(&()));
let bar = Bar::new("five", MyBar { bars: 5 });
println!("Spec: {:?}", bar.spec);
assert_impl_all!(Bar: kube::Resource, JsonSchema);

let crd = Bar::crd();
for v in crd.spec.versions {
assert!(v.schema.unwrap().open_api_v3_schema.is_some());
}
}
26 changes: 10 additions & 16 deletions examples/crd_derive_no_schema.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
#[cfg(not(feature = "schema"))]
use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::{
CustomResourceDefinition, CustomResourceValidation, JSONSchemaProps,
};
#[cfg(not(feature = "schema"))] use kube_derive::CustomResource;
#[cfg(not(feature = "schema"))] use serde::{Deserialize, Serialize};
use kube_derive::CustomResource;
use serde::{Deserialize, Serialize};

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

#[cfg(not(feature = "schema"))]
const MANUAL_SCHEMA: &'static str = r#"
const MANUAL_SCHEMA: &str = r#"
type: object
properties:
spec:
Expand All @@ -31,7 +32,6 @@ properties:
- bars
"#;

#[cfg(not(feature = "schema"))]
impl Bar {
fn crd_with_manual_schema() -> CustomResourceDefinition {
use kube::CustomResourceExt;
Expand All @@ -47,18 +47,12 @@ impl Bar {
}
}

#[cfg(not(feature = "schema"))]
fn main() {
let crd = Bar::crd_with_manual_schema();
println!("{}", serde_yaml::to_string(&crd).unwrap());
}
#[cfg(feature = "schema")]
fn main() {
eprintln!("This example it disabled when using the schema feature");
}

// Verify CustomResource derivable still
#[cfg(not(feature = "schema"))]
#[test]
fn verify_bar_is_a_custom_resource() {
use kube::Resource;
Expand Down
4 changes: 0 additions & 4 deletions kube-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ darling = "0.13.0"
[lib]
proc-macro = true

[features]
default = ["schema"]
schema = []

[dev-dependencies]
serde = { version = "1.0.130", features = ["derive"] }
serde_yaml = "0.8.21"
Expand Down
124 changes: 95 additions & 29 deletions kube-derive/src/custom_resource.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use darling::FromDeriveInput;
use darling::{FromDeriveInput, FromMeta};
use proc_macro2::{Ident, Span, TokenStream};
use syn::{parse_quote, Data, DeriveInput, Path, Visibility};

Expand All @@ -24,6 +24,8 @@ struct KubeAttrs {
#[darling(multiple, rename = "derive")]
derives: Vec<String>,
#[darling(default)]
schema: Option<SchemaMode>,
#[darling(default)]
status: Option<String>,
#[darling(multiple, rename = "category")]
categories: Vec<String>,
Expand All @@ -33,35 +35,92 @@ struct KubeAttrs {
printcolums: Vec<String>,
#[darling(default)]
scale: Option<String>,
#[darling(default = "default_kube_core")]
#[darling(default)]
crates: Crates,
}

#[derive(Debug, FromMeta)]
struct Crates {
#[darling(default = "Self::default_kube_core")]
kube_core: Path,
#[darling(default = "default_k8s_openapi")]
#[darling(default = "Self::default_k8s_openapi")]
k8s_openapi: Path,
#[darling(default = "default_schemars")]
#[darling(default = "Self::default_schemars")]
schemars: Path,
#[darling(default = "default_serde")]
#[darling(default = "Self::default_serde")]
serde: Path,
#[darling(default = "default_serde_json")]
#[darling(default = "Self::default_serde_json")]
serde_json: Path,
}

fn default_apiext() -> String {
"v1".to_owned()
// Default is required when the subattribute isn't mentioned at all
// Delegate to darling rather than deriving, so that we can piggyback off the `#[darling(default)]` clauses
impl Default for Crates {
fn default() -> Self {
Self::from_list(&[]).unwrap()
}
}
fn default_kube_core() -> Path {
parse_quote! { ::kube::core } // by default must work well with people using facade crate

impl Crates {
fn default_kube_core() -> Path {
parse_quote! { ::kube::core } // by default must work well with people using facade crate
}

fn default_k8s_openapi() -> Path {
parse_quote! { ::k8s_openapi }
}

fn default_schemars() -> Path {
parse_quote! { ::schemars }
}

fn default_serde() -> Path {
parse_quote! { ::serde }
}

fn default_serde_json() -> Path {
parse_quote! { ::serde_json }
}
}
fn default_k8s_openapi() -> Path {
parse_quote! { ::k8s_openapi }

fn default_apiext() -> String {
"v1".to_owned()
}
fn default_schemars() -> Path {
parse_quote! { ::schemars }

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum SchemaMode {
Disabled,
Manual,
Derived,
}
fn default_serde() -> Path {
parse_quote! { ::serde }

impl SchemaMode {
fn derive(self) -> bool {
match self {
SchemaMode::Disabled => false,
SchemaMode::Manual => false,
SchemaMode::Derived => true,
}
}

fn use_in_crd(self) -> bool {
match self {
SchemaMode::Disabled => false,
SchemaMode::Manual => true,
SchemaMode::Derived => true,
}
}
}
fn default_serde_json() -> Path {
parse_quote! { ::serde_json }

impl FromMeta for SchemaMode {
fn from_string(value: &str) -> darling::Result<Self> {
match value {
"disabled" => Ok(SchemaMode::Disabled),
"manual" => Ok(SchemaMode::Manual),
"derived" => Ok(SchemaMode::Derived),
x => Err(darling::Error::unknown_value(x)),
}
}
}

pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
Expand Down Expand Up @@ -92,6 +151,7 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea
version,
namespaced,
derives,
schema: schema_mode,
status,
plural,
singular,
Expand All @@ -100,11 +160,14 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea
printcolums,
apiextensions,
scale,
kube_core,
k8s_openapi,
schemars,
serde,
serde_json,
crates:
Crates {
kube_core,
k8s_openapi,
schemars,
serde,
serde_json,
},
} = kube_attrs;

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

// Schema generation is always enabled for v1 because it's mandatory.
// TODO Enable schema generation for v1beta1 if the spec derives `JsonSchema`.
let schema_gen_enabled = apiextensions == "v1" && cfg!(feature = "schema");
// Enable schema generation by default for v1 because it's mandatory.
let schema_mode = schema_mode.unwrap_or(if apiextensions == "v1" {
SchemaMode::Derived
} else {
SchemaMode::Disabled
});
// We exclude fields `apiVersion`, `kind`, and `metadata` from our schema because
// these are validated by the API server implicitly. Also, we can't generate the
// schema for `metadata` (`ObjectMeta`) because it doesn't implement `JsonSchema`.
let schemars_skip = if schema_gen_enabled {
let schemars_skip = if schema_mode.derive() {
quote! { #[schemars(skip)] }
} else {
quote! {}
};
if schema_gen_enabled {
if schema_mode.derive() {
derive_paths.push(syn::parse_quote! { #schemars::JsonSchema });
}

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

let schemagen = if schema_gen_enabled {
let schemagen = if schema_mode.use_in_crd() {
quote! {
// Don't use definitions and don't include `$schema` because these are not allowed.
let gen = #schemars::gen::SchemaSettings::openapi3().with(|s| {
Expand Down
Loading