Skip to content

Commit

Permalink
add example using validator - closes #129 (#647)
Browse files Browse the repository at this point in the history
* add example using validator - closes #129

Signed-off-by: clux <sszynrae@gmail.com>

* fix doc link, fmt, and ignore aggressive clippy warns

Signed-off-by: clux <sszynrae@gmail.com>

* account for choices and add warnings

Signed-off-by: clux <sszynrae@gmail.com>

* also validate locally and add comments here

Signed-off-by: clux <sszynrae@gmail.com>
  • Loading branch information
clux authored Oct 10, 2021
1 parent 6aa0fd3 commit 88c5616
Show file tree
Hide file tree
Showing 9 changed files with 65 additions and 14 deletions.
5 changes: 2 additions & 3 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
29 changes: 27 additions & 2 deletions examples/crd_api.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand All @@ -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,
Expand All @@ -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?;
Expand Down Expand Up @@ -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) => {
Expand Down
1 change: 1 addition & 0 deletions kube-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
30 changes: 26 additions & 4 deletions kube-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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)]
Expand Down Expand Up @@ -163,18 +166,37 @@ 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.
///
/// 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)
///
/// ## 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.
Expand Down
6 changes: 3 additions & 3 deletions kube-runtime/src/controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,13 +398,13 @@ where
{
// NB: Need to Unpin for stream::select_all
trigger_selector: stream::SelectAll<BoxStream<'static, Result<ReconcileRequest<K>, 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<BoxFuture<'static, ()>>,
/// [`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<BoxFuture<'static, ()>>,
dyntype: K::DynamicType,
reader: Store<K>,
Expand Down
4 changes: 3 additions & 1 deletion kube-runtime/src/finalizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -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<K, ReconcileFut>(
api: &Api<K>,
finalizer_name: &str,
Expand Down
2 changes: 1 addition & 1 deletion kube-runtime/src/wait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<K: Resource>(uid: &str) -> impl Fn(Option<&K>) -> bool + '_ {
move |obj: Option<&K>| {
Expand Down
1 change: 1 addition & 0 deletions kube/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub struct Api<K> {
/// Note: Using `iter::Empty` over `PhantomData`, because we never actually keep any
/// `K` objects, so `Empty` better models our constraints (in particular, `Empty<K>`
/// is `Send`, even if `K` may not be).
#[allow(dead_code)]
pub(crate) phantom: std::iter::Empty<K>,
}

Expand Down
1 change: 1 addition & 0 deletions kube/src/config/file_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ impl Kubeconfig {
}
}

#[allow(clippy::redundant_closure)]
fn append_new_named<T, F>(base: &mut Vec<T>, next: Vec<T>, f: F)
where
F: Fn(&T) -> &String,
Expand Down

0 comments on commit 88c5616

Please sign in to comment.