diff --git a/kube-core/src/labels.rs b/kube-core/src/labels.rs new file mode 100644 index 000000000..0c7a1a019 --- /dev/null +++ b/kube-core/src/labels.rs @@ -0,0 +1,634 @@ +#![allow(missing_docs)] +use core::fmt; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::{LabelSelector, LabelSelectorRequirement}; +use serde::{Deserialize, Serialize}; +use std::{ + cmp::PartialEq, + collections::{BTreeMap, BTreeSet}, + fmt::Display, + iter::FromIterator, + option::IntoIter, +}; +use thiserror::Error; + +mod private { + pub trait Sealed {} + impl Sealed for super::Expression {} + impl Sealed for super::Selector {} +} + +#[derive(Debug, Error)] +#[error("failed to parse value as expression: {0}")] +/// Indicates failure of conversion to Expression +pub struct ParseExpressionError(pub String); + +// local type aliases +type Expressions = Vec; + +/// Selector extension trait for querying selector like objects +/// +/// Only implemented by `Selector` and `Expression`. +pub trait SelectorExt: private::Sealed { + /// Collection type to compare with self + type Search; + + /// Perform a match check on the arbitrary components like labels + /// + /// ``` + /// use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector; + /// use crate::kube_core::SelectorExt; + /// use crate::kube_core::Selector; + /// use kube_core::ParseExpressionError; + /// + /// let selector: Selector = LabelSelector::default().try_into()?; + /// selector.matches(&Default::default()); + /// # Ok::<(), ParseExpressionError>(()) + /// ``` + fn matches(&self, on: &Self::Search) -> bool; +} + +/// A selector expression with existing operations +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub enum Expression { + /// Key exists and in set: + /// + /// ``` + /// use kube_core::Expression; + /// + /// let exp = Expression::In("foo".into(), ["bar".into(), "baz".into()].into()).to_string(); + /// assert_eq!(exp, "foo in (bar,baz)"); + /// let exp = Expression::In("foo".into(), vec!["bar".into(), "baz".into()].into_iter().collect()).to_string(); + /// assert_eq!(exp, "foo in (bar,baz)"); + /// ``` + In(String, BTreeSet), + + /// Key does not exists or not in set: + /// + /// ``` + /// use kube_core::Expression; + /// + /// let exp = Expression::NotIn("foo".into(), ["bar".into(), "baz".into()].into()).to_string(); + /// assert_eq!(exp, "foo notin (bar,baz)"); + /// let exp = Expression::NotIn("foo".into(), vec!["bar".into(), "baz".into()].into_iter().collect()).to_string(); + /// assert_eq!(exp, "foo notin (bar,baz)"); + /// ``` + NotIn(String, BTreeSet), + + /// Key exists and is equal: + /// + /// ``` + /// use kube_core::Expression; + /// + /// let exp = Expression::Equal("foo".into(), "bar".into()).to_string(); + /// assert_eq!(exp, "foo=bar") + /// ``` + Equal(String, String), + + /// Key does not exists or is not equal: + /// + /// ``` + /// use kube_core::Expression; + /// + /// let exp = Expression::NotEqual("foo".into(), "bar".into()).to_string(); + /// assert_eq!(exp, "foo!=bar") + /// ``` + NotEqual(String, String), + + /// Key exists: + /// + /// ``` + /// use kube_core::Expression; + /// + /// let exp = Expression::Exists("foo".into()).to_string(); + /// assert_eq!(exp, "foo") + /// ``` + Exists(String), + + /// Key does not exist: + /// + /// ``` + /// use kube_core::Expression; + /// + /// let exp = Expression::DoesNotExist("foo".into()).to_string(); + /// assert_eq!(exp, "!foo") + /// ``` + DoesNotExist(String), +} + +/// Perform selection on a list of expressions +#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Serialize)] +pub struct Selector(Expressions); + +impl Selector { + /// Create a selector from a vector of expressions + fn from_expressions(exprs: Expressions) -> Self { + Self(exprs) + } + + /// Create a selector from a map of key=value label matches + fn from_map(map: BTreeMap) -> Self { + Self(map.into_iter().map(|(k, v)| Expression::Equal(k, v)).collect()) + } + + /// Indicates whether this label selector matches all pods + pub fn selects_all(&self) -> bool { + self.0.is_empty() + } + + /// Extend the list of expressions for the selector + /// + /// ``` + /// use kube_core::Selector; + /// use kube_core::Expression; + /// use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector; + /// use kube_core::ParseExpressionError; + /// + /// let label_selector: Selector = LabelSelector::default().try_into()?; + /// let mut selector = &mut Selector::default(); + /// selector = selector.extend(Expression::Equal("environment".into(), "production".into())); + /// selector.extend([Expression::Exists("bar".into()), Expression::Exists("foo".into())].into_iter()); + /// selector.extend(label_selector); + /// # Ok::<(), ParseExpressionError>(()) + /// ``` + pub fn extend(&mut self, exprs: impl IntoIterator) -> &mut Self { + self.0.extend(exprs); + self + } +} + +impl SelectorExt for Selector { + type Search = BTreeMap; + + /// Perform a match check on the resource labels + fn matches(&self, labels: &BTreeMap) -> bool { + for expr in self.0.iter() { + if !expr.matches(labels) { + return false; + } + } + true + } +} + +impl SelectorExt for Expression { + type Search = BTreeMap; + + fn matches(&self, labels: &BTreeMap) -> bool { + match self { + Expression::In(key, values) => match labels.get(key) { + Some(v) => values.contains(v), + None => false, + }, + Expression::NotIn(key, values) => match labels.get(key) { + Some(v) => !values.contains(v), + None => true, + }, + Expression::Exists(key) => labels.contains_key(key), + Expression::DoesNotExist(key) => !labels.contains_key(key), + Expression::Equal(key, value) => labels.get(key) == Some(value), + Expression::NotEqual(key, value) => labels.get(key) != Some(value), + } + } +} + +impl Display for Expression { + /// Perform conversion to string + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Expression::In(key, values) => { + write!( + f, + "{key} in ({})", + values.iter().cloned().collect::>().join(",") + ) + } + Expression::NotIn(key, values) => { + write!( + f, + "{key} notin ({})", + values.iter().cloned().collect::>().join(",") + ) + } + Expression::Equal(key, value) => write!(f, "{key}={value}"), + Expression::NotEqual(key, value) => write!(f, "{key}!={value}"), + Expression::Exists(key) => write!(f, "{key}"), + Expression::DoesNotExist(key) => write!(f, "!{key}"), + } + } +} + +impl Display for Selector { + /// Convert a selector to a string for the API + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let selectors: Vec = self.0.iter().map(|e| e.to_string()).collect(); + write!(f, "{}", selectors.join(",")) + } +} +// convenience conversions for Selector and Expression + +impl IntoIterator for Expression { + type IntoIter = IntoIter; + type Item = Self; + + fn into_iter(self) -> Self::IntoIter { + Some(self).into_iter() + } +} + +impl IntoIterator for Selector { + type IntoIter = std::vec::IntoIter; + type Item = Expression; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl FromIterator<(String, String)> for Selector { + fn from_iter>(iter: T) -> Self { + Self::from_map(iter.into_iter().collect()) + } +} + +impl FromIterator<(&'static str, &'static str)> for Selector { + /// ``` + /// use kube_core::{Selector, Expression}; + /// + /// let sel: Selector = Some(("foo", "bar")).into_iter().collect(); + /// let equal: Selector = Expression::Equal("foo".into(), "bar".into()).into(); + /// assert_eq!(sel, equal) + /// ``` + fn from_iter>(iter: T) -> Self { + Self::from_map( + iter.into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + ) + } +} + +impl FromIterator for Selector { + fn from_iter>(iter: T) -> Self { + Self::from_expressions(iter.into_iter().collect()) + } +} + +impl From for Selector { + fn from(value: Expression) -> Self { + Self(vec![value]) + } +} + +impl TryFrom for Selector { + type Error = ParseExpressionError; + + fn try_from(value: LabelSelector) -> Result { + let expressions = match value.match_expressions { + Some(requirements) => requirements.into_iter().map(TryInto::try_into).collect(), + None => Ok(vec![]), + }?; + let mut equality: Selector = value + .match_labels + .map(|labels| labels.into_iter().collect()) + .unwrap_or_default(); + equality.extend(expressions); + Ok(equality) + } +} + +impl TryFrom for Expression { + type Error = ParseExpressionError; + + fn try_from(requirement: LabelSelectorRequirement) -> Result { + let key = requirement.key; + let values = requirement.values.map(|values| values.into_iter().collect()); + match requirement.operator.as_str() { + "In" => match values { + Some(values) => Ok(Expression::In(key, values)), + None => Err(ParseExpressionError( + "Expected values for In operator, got none".into(), + )), + }, + "NotIn" => match values { + Some(values) => Ok(Expression::NotIn(key, values)), + None => Err(ParseExpressionError( + "Expected values for In operator, got none".into(), + )), + }, + "Exists" => Ok(Expression::Exists(key)), + "DoesNotExist" => Ok(Expression::DoesNotExist(key)), + _ => Err(ParseExpressionError("Invalid expression operator".into())), + } + } +} + +impl From for LabelSelector { + fn from(value: Selector) -> Self { + let mut equality = vec![]; + let mut expressions = vec![]; + for expr in value.0 { + match expr { + Expression::In(key, values) => expressions.push(LabelSelectorRequirement { + key, + operator: "In".into(), + values: Some(values.into_iter().collect()), + }), + Expression::NotIn(key, values) => expressions.push(LabelSelectorRequirement { + key, + operator: "NotIn".into(), + values: Some(values.into_iter().collect()), + }), + Expression::Equal(key, value) => equality.push((key, value)), + Expression::NotEqual(key, value) => expressions.push(LabelSelectorRequirement { + key, + operator: "NotIn".into(), + values: Some(vec![value]), + }), + Expression::Exists(key) => expressions.push(LabelSelectorRequirement { + key, + operator: "Exists".into(), + values: None, + }), + Expression::DoesNotExist(key) => expressions.push(LabelSelectorRequirement { + key, + operator: "DoesNotExist".into(), + values: None, + }), + } + } + + LabelSelector { + match_labels: (!equality.is_empty()).then_some(equality.into_iter().collect()), + match_expressions: (!expressions.is_empty()).then_some(expressions), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::iter::FromIterator; + + #[test] + fn test_raw_matches() { + for (selector, label_selector, labels, matches, msg) in &[ + ( + Selector::default(), + LabelSelector::default(), + Default::default(), + true, + "empty match", + ), + ( + Selector::from_iter(Some(("foo", "bar"))), + LabelSelector { + match_labels: Some([("foo".into(), "bar".into())].into()), + match_expressions: Default::default(), + }, + [("foo".to_string(), "bar".to_string())].into(), + true, + "exact label match", + ), + ( + Selector::from_iter(Some(("foo", "bar"))), + LabelSelector { + match_labels: Some([("foo".to_string(), "bar".to_string())].into()), + match_expressions: None, + }, + [ + ("foo".to_string(), "bar".to_string()), + ("bah".to_string(), "baz".to_string()), + ] + .into(), + true, + "sufficient label match", + ), + ( + Selector::from_iter(Some(Expression::In( + "foo".into(), + Some("bar".to_string()).into_iter().collect(), + ))), + LabelSelector { + match_labels: None, + match_expressions: Some(vec![LabelSelectorRequirement { + key: "foo".into(), + operator: "In".to_string(), + values: Some(vec!["bar".into()]), + }]), + }, + [ + ("foo".to_string(), "bar".to_string()), + ("bah".to_string(), "baz".to_string()), + ] + .into(), + true, + "In expression match", + ), + ( + Selector::from_iter(Some(Expression::Equal( + "foo".into(), + Some("bar".to_string()).into_iter().collect(), + ))), + LabelSelector { + match_labels: Some([("foo".into(), "bar".into())].into()), + match_expressions: None, + }, + [ + ("foo".to_string(), "bar".to_string()), + ("bah".to_string(), "baz".to_string()), + ] + .into(), + true, + "Equal expression match", + ), + ( + Selector::from_iter(Some(Expression::NotEqual( + "foo".into(), + Some("bar".to_string()).into_iter().collect(), + ))), + LabelSelector { + match_labels: None, + match_expressions: Some(vec![LabelSelectorRequirement { + key: "foo".into(), + operator: "NotIn".into(), + values: Some(vec!["bar".into()]), + }]), + }, + [ + ("foo".to_string(), "bar".to_string()), + ("bah".to_string(), "baz".to_string()), + ] + .into(), + false, + "NotEqual expression match", + ), + ( + Selector::from_iter(Some(Expression::In( + "foo".into(), + Some("bar".to_string()).into_iter().collect(), + ))), + LabelSelector { + match_labels: None, + match_expressions: Some(vec![LabelSelectorRequirement { + key: "foo".into(), + operator: "In".into(), + values: Some(vec!["bar".into()]), + }]), + }, + [ + ("foo".to_string(), "bar".to_string()), + ("bah".to_string(), "baz".to_string()), + ] + .into(), + true, + "In expression match", + ), + ( + Selector::from_iter(Some(Expression::NotIn( + "foo".into(), + Some("quux".to_string()).into_iter().collect(), + ))), + LabelSelector { + match_labels: None, + match_expressions: Some(vec![LabelSelectorRequirement { + key: "foo".into(), + operator: "NotIn".into(), + values: Some(vec!["quux".into()]), + }]), + }, + [ + ("foo".to_string(), "bar".to_string()), + ("bah".to_string(), "baz".to_string()), + ] + .into(), + true, + "NotIn expression match", + ), + ( + Selector::from_iter(Some(Expression::NotIn( + "foo".into(), + Some("bar".to_string()).into_iter().collect(), + ))), + LabelSelector { + match_labels: None, + match_expressions: Some(vec![LabelSelectorRequirement { + key: "foo".into(), + operator: "NotIn".into(), + values: Some(vec!["bar".into()]), + }]), + }, + [ + ("foo".to_string(), "bar".to_string()), + ("bah".to_string(), "baz".to_string()), + ] + .into(), + false, + "NotIn expression non-match", + ), + ( + Selector(vec![ + Expression::Equal("foo".to_string(), "bar".to_string()), + Expression::In("bah".into(), Some("bar".to_string()).into_iter().collect()), + ]), + LabelSelector { + match_labels: Some([("foo".into(), "bar".into())].into()), + match_expressions: Some(vec![LabelSelectorRequirement { + key: "bah".into(), + operator: "In".into(), + values: Some(vec!["bar".into()]), + }]), + }, + [ + ("foo".to_string(), "bar".to_string()), + ("bah".to_string(), "baz".to_string()), + ] + .into(), + false, + "matches labels but not expressions", + ), + ( + Selector(vec![ + Expression::Equal("foo".to_string(), "bar".to_string()), + Expression::In("bah".into(), Some("bar".to_string()).into_iter().collect()), + ]), + LabelSelector { + match_labels: Some([("foo".into(), "bar".into())].into()), + match_expressions: Some(vec![LabelSelectorRequirement { + key: "bah".into(), + operator: "In".into(), + values: Some(vec!["bar".into()]), + }]), + }, + [ + ("foo".to_string(), "bar".to_string()), + ("bah".to_string(), "bar".to_string()), + ] + .into(), + true, + "matches both labels and expressions", + ), + ] { + assert_eq!(selector.matches(labels), *matches, "{}", msg); + let converted: LabelSelector = selector.clone().into(); + assert_eq!(&converted, label_selector); + let converted_selector: Selector = converted.try_into().unwrap(); + assert_eq!( + converted_selector.matches(labels), + *matches, + "After conversion: {}", + msg + ); + } + } + + #[test] + fn test_label_selector_matches() { + let selector: Selector = LabelSelector { + match_expressions: Some(vec![ + LabelSelectorRequirement { + key: "foo".into(), + operator: "In".into(), + values: Some(vec!["bar".into()]), + }, + LabelSelectorRequirement { + key: "foo".into(), + operator: "NotIn".into(), + values: Some(vec!["baz".into()]), + }, + LabelSelectorRequirement { + key: "foo".into(), + operator: "Exists".into(), + values: None, + }, + LabelSelectorRequirement { + key: "baz".into(), + operator: "DoesNotExist".into(), + values: None, + }, + ]), + match_labels: Some([("foo".into(), "bar".into())].into()), + } + .try_into() + .unwrap(); + assert!(selector.matches(&[("foo".into(), "bar".into())].into())); + assert!(!selector.matches(&Default::default())); + } + + #[test] + fn test_to_string() { + let selector = Selector(vec![ + Expression::In("foo".into(), ["bar".into(), "baz".into()].into()), + Expression::NotIn("foo".into(), ["bar".into(), "baz".into()].into()), + Expression::Equal("foo".into(), "bar".into()), + Expression::NotEqual("foo".into(), "bar".into()), + Expression::Exists("foo".into()), + Expression::DoesNotExist("foo".into()), + ]) + .to_string(); + + assert_eq!( + selector, + "foo in (bar,baz),foo notin (bar,baz),foo=bar,foo!=bar,foo,!foo" + ) + } +} diff --git a/kube-core/src/lib.rs b/kube-core/src/lib.rs index 4e87ed752..93d0caca1 100644 --- a/kube-core/src/lib.rs +++ b/kube-core/src/lib.rs @@ -31,6 +31,8 @@ pub use gvk::{GroupVersion, GroupVersionKind, GroupVersionResource}; pub mod metadata; pub use metadata::{ListMeta, ObjectMeta, PartialObjectMeta, PartialObjectMetaExt, TypeMeta}; +pub mod labels; + #[cfg(feature = "kubelet-debug")] pub mod kubelet_debug; pub mod object; @@ -50,6 +52,8 @@ pub use resource::{ pub mod response; pub use response::Status; +pub use labels::{Expression, ParseExpressionError, Selector, SelectorExt}; + #[cfg_attr(docsrs, doc(cfg(feature = "schema")))] #[cfg(feature = "schema")] pub mod schema; diff --git a/kube-core/src/params.rs b/kube-core/src/params.rs index 01136c669..c01b82823 100644 --- a/kube-core/src/params.rs +++ b/kube-core/src/params.rs @@ -1,5 +1,5 @@ //! A port of request parameter *Optionals from apimachinery/types.go -use crate::request::Error; +use crate::{request::Error, Selector}; use serde::Serialize; /// Controls how the resource version parameter is applied for list calls @@ -166,6 +166,28 @@ impl ListParams { self } + /// Configure typed label selectors + /// + /// Configure typed selectors from [`Selector`](crate::Selector) and [`Expression`](crate::Expression) lists. + /// + /// ``` + /// use kube::api::ListParams; + /// use kube_core::{Expression, Selector, ParseExpressionError}; + /// use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector; + /// let selector: Selector = Expression::In("env".into(), ["development".into(), "sandbox".into()].into()).into(); + /// let lp = ListParams::default().labels_from(&selector); + /// let lp = ListParams::default().labels_from(&Expression::Exists("foo".into()).into()); + /// // Alternatively the raw LabelSelector is accepted + /// let selector: Selector = LabelSelector::default().try_into()?; + /// let lp = ListParams::default().labels_from(&selector); + /// # Ok::<(), ParseExpressionError>(()) + ///``` + #[must_use] + pub fn labels_from(mut self, selector: &Selector) -> Self { + self.label_selector = Some(selector.to_string()); + self + } + /// Sets a result limit. #[must_use] pub fn limit(mut self, limit: u32) -> Self { @@ -429,6 +451,28 @@ impl WatchParams { self } + /// Configure typed label selectors + /// + /// Configure typed selectors from [`Selector`](crate::Selector) and [`Expression`](crate::Expression) lists. + /// + /// ``` + /// use kube::api::WatchParams; + /// use kube_core::{Expression, Selector, ParseExpressionError}; + /// use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector; + /// let selector: Selector = Expression::In("env".into(), ["development".into(), "sandbox".into()].into()).into(); + /// let wp = WatchParams::default().labels_from(&selector); + /// let wp = WatchParams::default().labels_from(&Expression::Exists("foo".into()).into()); + /// // Alternatively the raw LabelSelector is accepted + /// let selector: Selector = LabelSelector::default().try_into()?; + /// let wp = WatchParams::default().labels_from(&selector); + /// # Ok::<(), ParseExpressionError>(()) + ///``` + #[must_use] + pub fn labels_from(mut self, selector: &Selector) -> Self { + self.label_selector = Some(selector.to_string()); + self + } + /// Disables watch bookmarks to simplify watch handling /// /// This is not recommended to use with production watchers as it can cause desyncs. @@ -826,7 +870,9 @@ where } #[cfg(test)] mod test { - use super::{DeleteParams, PatchParams}; + use crate::{params::WatchParams, Expression, Selector}; + + use super::{DeleteParams, ListParams, PatchParams}; #[test] fn delete_param_serialize() { let mut dp = DeleteParams::default(); @@ -875,6 +921,24 @@ mod test { let urlstr = qp.finish(); assert_eq!(String::from("some/resource?&fieldValidation=Strict"), urlstr); } + + #[test] + fn list_params_serialize() { + let selector: Selector = + Expression::In("env".into(), ["development".into(), "sandbox".into()].into()).into(); + let lp = ListParams::default().labels_from(&selector); + let labels = lp.label_selector.unwrap(); + assert_eq!(labels, "env in (development,sandbox)"); + } + + #[test] + fn watch_params_serialize() { + let selector: Selector = + Expression::In("env".into(), ["development".into(), "sandbox".into()].into()).into(); + let wp = WatchParams::default().labels_from(&selector); + let labels = wp.label_selector.unwrap(); + assert_eq!(labels, "env in (development,sandbox)"); + } } /// Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out. diff --git a/kube-runtime/src/watcher.rs b/kube-runtime/src/watcher.rs index e259e154a..6d064cd42 100644 --- a/kube-runtime/src/watcher.rs +++ b/kube-runtime/src/watcher.rs @@ -9,7 +9,7 @@ use derivative::Derivative; use futures::{stream::BoxStream, Stream, StreamExt}; use kube_client::{ api::{ListParams, Resource, ResourceExt, VersionMatch, WatchEvent, WatchParams}, - core::{metadata::PartialObjectMeta, ObjectList}, + core::{metadata::PartialObjectMeta, ObjectList, Selector}, error::ErrorResponse, Api, Error as ClientErr, }; @@ -331,6 +331,27 @@ impl Config { self } + /// Configure typed label selectors + /// + /// Configure typed selectors from [`Selector`](kube_client::core::Selector) and [`Expression`](kube_client::core::Expression) lists. + /// + /// ``` + /// use kube_runtime::watcher::Config; + /// use kube_client::core::{Expression, Selector, ParseExpressionError}; + /// use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector; + /// let selector: Selector = Expression::In("env".into(), ["development".into(), "sandbox".into()].into()).into(); + /// let cfg = Config::default().labels_from(&selector); + /// let cfg = Config::default().labels_from(&Expression::Exists("foo".into()).into()); + /// let selector: Selector = LabelSelector::default().try_into()?; + /// let cfg = Config::default().labels_from(&selector); + /// # Ok::<(), ParseExpressionError>(()) + ///``` + #[must_use] + pub fn labels_from(mut self, selector: &Selector) -> Self { + self.label_selector = Some(selector.to_string()); + self + } + /// Sets list semantic to configure re-list performance and consistency /// /// NB: This option only has an effect for [`InitialListStrategy::ListWatch`]. diff --git a/kube/src/lib.rs b/kube/src/lib.rs index 52074ff3a..c94a8bb31 100644 --- a/kube/src/lib.rs +++ b/kube/src/lib.rs @@ -199,6 +199,7 @@ pub mod prelude { #[cfg(feature = "unstable-client")] pub use crate::client::scope::NamespacedRef; #[allow(unreachable_pub)] pub use crate::core::PartialObjectMetaExt as _; + #[allow(unreachable_pub)] pub use crate::core::SelectorExt as _; pub use crate::{core::crd::CustomResourceExt as _, Resource as _, ResourceExt as _}; #[cfg(feature = "runtime")] pub use crate::runtime::utils::WatchStreamExt as _;