diff --git a/CHANGELOG.md b/CHANGELOG.md index 08ead9c26..28f3ea84d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 =================== diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 7f762faac..1c1159392 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -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"] @@ -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" diff --git a/examples/crd_derive_custom_schema.rs b/examples/crd_derive_custom_schema.rs new file mode 100644 index 000000000..c2e8e74c3 --- /dev/null +++ b/examples/crd_derive_custom_schema.rs @@ -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()); + } +} diff --git a/examples/crd_derive_no_schema.rs b/examples/crd_derive_no_schema.rs index 89bd95c86..4795e8598 100644 --- a/examples/crd_derive_no_schema.rs +++ b/examples/crd_derive_no_schema.rs @@ -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: @@ -31,7 +32,6 @@ properties: - bars "#; -#[cfg(not(feature = "schema"))] impl Bar { fn crd_with_manual_schema() -> CustomResourceDefinition { use kube::CustomResourceExt; @@ -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; diff --git a/kube-derive/Cargo.toml b/kube-derive/Cargo.toml index eb44d65bf..d333d9e12 100644 --- a/kube-derive/Cargo.toml +++ b/kube-derive/Cargo.toml @@ -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" diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index 2caa5712e..c8c4fb223 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -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}; @@ -24,6 +24,8 @@ struct KubeAttrs { #[darling(multiple, rename = "derive")] derives: Vec, #[darling(default)] + schema: Option, + #[darling(default)] status: Option, #[darling(multiple, rename = "category")] categories: Vec, @@ -33,35 +35,92 @@ struct KubeAttrs { printcolums: Vec, #[darling(default)] scale: Option, - #[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 { + 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 { @@ -92,6 +151,7 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea version, namespaced, derives, + schema: schema_mode, status, plural, singular, @@ -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()); @@ -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 }); } @@ -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| { diff --git a/kube-derive/src/lib.rs b/kube-derive/src/lib.rs index 205374151..2f7215482 100644 --- a/kube-derive/src/lib.rs +++ b/kube-derive/src/lib.rs @@ -87,20 +87,20 @@ mod custom_resource; /// ### `#[kube(struct = "StructName")]` /// Customize the name of the generated root struct (defaults to `kind`). /// -/// ### `#[kube(kube_core = "::kube::core")]` +/// ### `#[kube(crates(kube_core = "::kube::core"))]` /// Customize the crate name the generated code will reach into (defaults to `::kube::core`). /// Should be one of `kube::core`, `kube_client::core` or `kube_core`. /// -/// ### `#[kube(k8s_openapi = "::k8s_openapi")]` +/// ### `#[kube(crates(k8s_openapi = "::k8s_openapi"))]` /// Customize the crate name the generated code will use for [`k8s_openapi`](https://docs.rs/k8s-openapi/) (defaults to `::k8s_openapi`). /// -/// ### `#[kube(schemars = "::schemars")]` +/// ### `#[kube(crates(schemars = "::schemars"))]` /// Customize the crate name the generated code will use for [`schemars`](https://docs.rs/schemars/) (defaults to `::schemars`). /// -/// ### `#[kube(serde = "::serde")]` +/// ### `#[kube(crates(serde = "::serde"))]` /// Customize the crate name the generated code will use for [`serde`](https://docs.rs/serde/) (defaults to `::serde`). /// -/// ### `#[kube(serde_json = "::serde_json")]` +/// ### `#[kube(crates(serde_json = "::serde_json"))]` /// Customize the crate name the generated code will use for [`serde_json`](https://docs.rs/serde_json/) (defaults to `::serde_json`). /// /// ### `#[kube(status = "StatusStructName")]` @@ -111,6 +111,22 @@ mod custom_resource; /// Adding `#[kube(derive = "PartialEq")]` is required if you want your generated /// top level type to be able to `#[derive(PartialEq)]` /// +/// ### `#[kube(schema = "mode")]` +/// Defines whether the `JsonSchema` of the top level generated type should be used when generating a `CustomResourceDefinition`. +/// +/// Legal values: +/// - `"derived"`: A `JsonSchema` implementation is automatically derived +/// - `"manual"`: `JsonSchema` is not derived, but used when creating the `CustomResourceDefinition` object +/// - `"disabled"`: No `JsonSchema` is used +/// +/// This can be used to provide a completely custom schema, or to interact with third-party custom resources +/// where you are not responsible for installing the `CustomResourceDefinition`. +/// +/// Defaults to `"disabled"` when `apiextensions = "v1beta1"`, otherwise `"derived"`. +/// +/// NOTE: `apiextensions = "v1"` `CustomResourceDefinition`s require a schema. If `schema = "disabled"` then +/// `Self::crd()` will not be installable into the cluster as-is. +/// /// ### `#[kube(scale = r#"json"#)]` /// Allow customizing the scale struct for the [scale subresource](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#subresources). /// @@ -190,7 +206,7 @@ mod custom_resource; /// /// See [kubernetes openapi validation](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation) for the format of the OpenAPI v3 schemas. /// -/// If you have to override a lot, [you can opt-out of schema-generation entirely](https://github.com/kube-rs/kube-rs/issues/355#issuecomment-751253657) +/// If you have to override a lot, [you can opt-out of schema-generation entirely](#kubeschema--mode) /// /// ## Advanced Features /// - **embedding k8s-openapi types** can be done by enabling the `schemars` feature of `k8s-openapi` from [`0.13.0`](https://github.com/Arnavion/k8s-openapi/blob/master/CHANGELOG.md#v0130-2021-08-09)