diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 6b3b6f8fa..fa6f800e3 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -23,11 +23,10 @@ ws = ["kube/ws"] latest = ["k8s-openapi/v1_22"] deprecated = ["kube/deprecated-crd-v1beta1", "k8s-openapi/v1_21"] -[dependencies] +[dev-dependencies] tokio-util = "0.6.8" assert-json-diff = "2.0.1" - -[dev-dependencies] +validator = { version = "0.14.0", features = ["derive"] } anyhow = "1.0.44" env_logger = "0.9.0" futures = "0.3.17" diff --git a/examples/crd_api.rs b/examples/crd_api.rs index 513179c79..e6910b014 100644 --- a/examples/crd_api.rs +++ b/examples/crd_api.rs @@ -1,10 +1,12 @@ #[macro_use] extern crate log; +use anyhow::{bail, Result}; use either::Either::{Left, Right}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::json; use std::time::Duration; use tokio::time::sleep; +use validator::Validate; // Using the old v1beta1 extension requires the deprecated-crd-v1beta1 feature on kube #[cfg(feature = "deprecated")] @@ -23,13 +25,14 @@ use kube::{ }; // Own custom resource -#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, Validate, JsonSchema)] #[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)] #[cfg_attr(feature = "deprecated", kube(apiextensions = "v1beta1"))] #[kube(status = "FooStatus")] #[kube(scale = r#"{"specReplicasPath":".spec.replicas", "statusReplicasPath":".status.replicas"}"#)] #[kube(printcolumn = r#"{"name":"Team", "jsonPath": ".spec.metadata.team", "type": "string"}"#)] pub struct FooSpec { + #[validate(length(min = 3))] name: String, info: String, replicas: i32, @@ -42,7 +45,7 @@ pub struct FooStatus { } #[tokio::main] -async fn main() -> anyhow::Result<()> { +async fn main() -> Result<()> { std::env::set_var("RUST_LOG", "info,kube=debug"); env_logger::init(); let client = Client::try_default().await?; @@ -203,6 +206,28 @@ async fn main() -> anyhow::Result<()> { // Delete the last - expect a status back (instant delete) assert!(foos.delete("qux", &dp).await?.is_right()); + // Check that validation is being obeyed + info!("Verifying validation rules"); + let fx = Foo::new("x", FooSpec { + name: "x".into(), + info: "failing validation obj".into(), + replicas: 1, + }); + // using derived Validate rules locally: + assert!(fx.spec.validate().is_err()); + // check rejection from apiserver (validation rules embedded in JsonSchema) + match foos.create(&pp, &fx).await { + Err(kube::Error::Api(ae)) => { + assert_eq!(ae.code, 422); + assert!(ae + .message + .contains("spec.name in body should be at least 3 chars long")); + } + Err(e) => bail!("somehow got unexpected error from validation: {:?}", e), + Ok(o) => bail!("somehow created {:?} despite validation", o), + } + info!("Rejected fx for invalid name {}", fx.name()); + // Cleanup the full collection - expect a wait match foos.delete_collection(&dp, &lp).await? { Left(list) => { diff --git a/kube-derive/Cargo.toml b/kube-derive/Cargo.toml index 56f807358..2b9f211dd 100644 --- a/kube-derive/Cargo.toml +++ b/kube-derive/Cargo.toml @@ -31,5 +31,6 @@ serde_yaml = "0.8.21" kube = { path = "../kube", default-features = false } k8s-openapi = { version = "0.13.1", default-features = false, features = ["v1_22"] } schemars = { version = "0.8.6", features = ["chrono"] } +validator = { version = "0.14.0", features = ["derive"] } chrono = "0.4.19" trybuild = "1.0.48" diff --git a/kube-derive/src/lib.rs b/kube-derive/src/lib.rs index a767721bd..41704e6f9 100644 --- a/kube-derive/src/lib.rs +++ b/kube-derive/src/lib.rs @@ -110,8 +110,9 @@ mod custom_resource; /// use serde::{Serialize, Deserialize}; /// use kube_derive::CustomResource; /// use schemars::JsonSchema; +/// use validator::Validate; /// -/// #[derive(CustomResource, Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)] +/// #[derive(CustomResource, Serialize, Deserialize, Debug, PartialEq, Clone, Validate, JsonSchema)] /// #[kube( /// group = "clux.dev", /// version = "v1", @@ -126,9 +127,11 @@ mod custom_resource; /// scale = r#"{"specReplicasPath":".spec.replicas", "statusReplicasPath":".status.replicas"}"#, /// printcolumn = r#"{"name":"Spec", "type":"string", "description":"name of foo", "jsonPath":".spec.name"}"# /// )] +/// #[serde(rename_all = "camelCase")] /// struct FooSpec { +/// #[validate(length(min = 3))] /// data: String, -/// replicas: i32 +/// replicas_count: i32 /// } /// /// #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)] @@ -163,11 +166,11 @@ mod custom_resource; /// - [Serde/Schemars Attributes](https://graham.cool/schemars/examples/3-schemars_attrs/) (no need to duplicate serde renames) /// - [`#[schemars(schema_with = "func")]`](https://graham.cool/schemars/examples/7-custom_serialization/) (e.g. like in the [`crd_derive` example](https://github.com/kube-rs/kube-rs/blob/master/examples/crd_derive.rs)) /// - `impl JsonSchema` on a type / newtype around external type. See [#129](https://github.com/kube-rs/kube-rs/issues/129#issuecomment-750852916) +/// - [`#[validate(...)]` field attributes with validator](https://github.com/Keats/validator) for kubebuilder style validation rules (see [`crd_api` example](https://github.com/kube-rs/kube-rs/blob/master/examples/crd_api.rs))) /// -/// In general, you will need to override parts of the schemas (for fields in question) when you are: +/// You might need to override parts of the schemas (for fields in question) when you are: /// - **using complex enums**: enums do not currently generate [structural schemas](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema), so kubernetes won't support them by default /// - **customizing [merge-strategies](https://kubernetes.io/docs/reference/using-api/server-side-apply/#merge-strategy)** (e.g. like in the [`crd_derive_schema` example](https://github.com/kube-rs/kube-rs/blob/master/examples/crd_derive_schema.rs)) -/// - **customizing [certain kubebuilder like validation rules](https://github.com/kube-rs/kube-rs/issues/129#issuecomment-749463718)** (tail the issue for state of affairs) /// /// 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. /// @@ -175,6 +178,25 @@ mod custom_resource; /// /// ## 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) +/// - **adding validation** via [validator crate](https://github.com/Keats/validator) is supported from `schemars` >= [`0.8.5`](https://github.com/GREsau/schemars/blob/master/CHANGELOG.md#085---2021-09-20) +/// +/// ### Validation Caveats +/// The supported **`#[validate]` attrs also exist as `#[schemars]` attrs** so you can use those directly if you do not require the validation to run client-side (in your code). +/// Otherwise, you should `#[derive(Validate)]` on your struct to have both server-side (kubernetes) and client-side validation. +/// +/// When using `validator` directly, you must add it to your dependencies (with the `derive` feature). +/// +/// Make sure your validation rules are static and handled by `schemars`: +/// - validations from `#[validate(custom = "some_fn")]` will not show up in the schema. +/// - similarly; [nested / must_match / credit_card were unhandled by schemars at time of writing](https://github.com/GREsau/schemars/pull/78) +/// +/// For sanity, you should review the generated schema before sending it to kubernetes. +/// +/// ## Versioning +/// Note that any changes to your struct / validation rules / serialization attributes will require you to re-apply the generated +/// schema to kubernetes, so that the apiserver can validate against the right version of your structs. +/// +/// How to best deal with version changes has not been fully sketched out. See [#569](https://github.com/kube-rs/kube-rs/issues/569). /// /// ## Debugging /// Try `cargo-expand` to see your own macro expansion. diff --git a/kube-runtime/src/controller/mod.rs b/kube-runtime/src/controller/mod.rs index f8e6d897c..07a7e88f0 100644 --- a/kube-runtime/src/controller/mod.rs +++ b/kube-runtime/src/controller/mod.rs @@ -398,13 +398,13 @@ where { // NB: Need to Unpin for stream::select_all trigger_selector: stream::SelectAll, watcher::Error>>>, - /// [`run`] starts a graceful shutdown when any of these [`Future`]s complete, + /// [`run`](crate::Controller::run) starts a graceful shutdown when any of these [`Future`]s complete, /// refusing to start any new reconciliations but letting any existing ones finish. graceful_shutdown_selector: Vec>, - /// [`run`] terminates immediately when any of these [`Future`]s complete, + /// [`run`](crate::Controller::run) terminates immediately when any of these [`Future`]s complete, /// requesting that all running reconciliations be aborted. /// However, note that they *will* keep running until their next yield point (`.await`), - /// blocking [`tokio::runtime::Runtime`] destruction (unless you follow up by calling [`std::process:exit`] after `run`). + /// blocking [`tokio::runtime::Runtime`] destruction (unless you follow up by calling [`std::process::exit`] after `run`). forceful_shutdown_selector: Vec>, dyntype: K::DynamicType, reader: Store, diff --git a/kube-runtime/src/finalizer.rs b/kube-runtime/src/finalizer.rs index 66923ce21..26cba0991 100644 --- a/kube-runtime/src/finalizer.rs +++ b/kube-runtime/src/finalizer.rs @@ -50,7 +50,7 @@ impl FinalizerState { /// cleanup is done. /// /// In typical usage, if you use `finalizer` then it should be the only top-level "action" -/// in your [`applier`]/[`Controller`]'s `reconcile` function. +/// in your [`applier`](crate::applier)/[`Controller`](crate::Controller)'s `reconcile` function. /// /// # Expected Flow /// @@ -94,6 +94,8 @@ impl FinalizerState { /// /// In addition, adding and removing the finalizer itself may fail. In particular, this may be because of /// network errors, lacking permissions, or because another `finalizer` was updated in the meantime on the same object. +/// +/// [`ObjectMeta::finalizers`]: kube::api::ObjectMeta#structfield.finalizers pub async fn finalizer( api: &Api, finalizer_name: &str, diff --git a/kube-runtime/src/wait.rs b/kube-runtime/src/wait.rs index 57fc7a918..0fecb42dc 100644 --- a/kube-runtime/src/wait.rs +++ b/kube-runtime/src/wait.rs @@ -55,7 +55,7 @@ pub mod conditions { /// An await condition that returns `true` once the object has been deleted. /// /// An object is considered to be deleted if the object can no longer be found, or if its - /// [`uid`] changes. This means that an object is considered to be deleted even if we miss + /// [`uid`](kube::api::ObjectMeta#structfield.uid) changes. This means that an object is considered to be deleted even if we miss /// the deletion event and the object is recreated in the meantime. pub fn is_deleted(uid: &str) -> impl Fn(Option<&K>) -> bool + '_ { move |obj: Option<&K>| { diff --git a/kube/src/api/mod.rs b/kube/src/api/mod.rs index 2a0bb12ad..66fda2e04 100644 --- a/kube/src/api/mod.rs +++ b/kube/src/api/mod.rs @@ -47,6 +47,7 @@ pub struct Api { /// Note: Using `iter::Empty` over `PhantomData`, because we never actually keep any /// `K` objects, so `Empty` better models our constraints (in particular, `Empty` /// is `Send`, even if `K` may not be). + #[allow(dead_code)] pub(crate) phantom: std::iter::Empty, } diff --git a/kube/src/config/file_config.rs b/kube/src/config/file_config.rs index eae7557d9..b876b026c 100644 --- a/kube/src/config/file_config.rs +++ b/kube/src/config/file_config.rs @@ -327,6 +327,7 @@ impl Kubeconfig { } } +#[allow(clippy::redundant_closure)] fn append_new_named(base: &mut Vec, next: Vec, f: F) where F: Fn(&T) -> &String,