Skip to content
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

add example using validator - closes #129 #647

Merged
merged 4 commits into from
Oct 10, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
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
26 changes: 24 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,25 @@ 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,
});
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