diff --git a/kube-core/src/labels.rs b/kube-core/src/labels.rs index 449c118ba..fd0b66bca 100644 --- a/kube-core/src/labels.rs +++ b/kube-core/src/labels.rs @@ -1,25 +1,119 @@ #![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 crate::ResourceExt; + // local type aliases type Map = BTreeMap; type Expressions = Vec; +/// Extensions to [`ResourceExt`](crate::ResourceExt) +/// Helper methods for resource selection based on provided Selector +pub trait SelectorExt { + fn selector_map(&self) -> ⤅ + + /// Perform a match on the resource using Matcher trait + /// + /// ``` + /// use k8s_openapi::api::core::v1::Pod; + /// use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector; + /// use kube_core::SelectorExt; + /// use kube_core::Expression; + /// let matches = Pod::default().selector_matches(LabelSelector::default()); + /// assert!(matches); + /// let matches = Pod::default().selector_matches(Expression::Exists("foo".into())); + /// assert!(!matches); + /// ``` + fn selector_matches(&self, selector: impl Matcher) -> bool { + selector.matches(self.selector_map()) + } +} + +impl SelectorExt for R { + fn selector_map(&self) -> &Map { + self.labels() + } +} + +/// Matcher trait for implementing alternalive Selectors +pub trait Matcher { + // Perform a match check on the resource labels + fn matches(&self, labels: &Map) -> 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)") + /// ``` 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)") + /// ``` 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), + + /// Invalid combination. Always evaluates to false. Invalid, } @@ -38,23 +132,39 @@ impl Selector { Self(map.into_iter().map(|(k, v)| Expression::Equal(k, v)).collect()) } - /// Convert a selector to a string for the API - pub fn to_selector_string(&self) -> String { - let selectors: Vec = self - .0 - .iter() - .filter(|&e| e != &Expression::Invalid) - .map(|e| e.to_string()) - .collect(); - selectors.join(",") - } - /// Indicates whether this label selector matches all pods pub fn selects_all(&self) -> bool { self.0.is_empty() } - pub fn matches(&self, labels: &Map) -> bool { + /// 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; + /// + /// Selector::default().extend(Expression::Exists("foo".into())); + /// Selector::default().extend(Selector::from_iter(Expression::Exists("foo".into()))); + /// let label_selector: Selector = LabelSelector::default().into(); + /// Selector::default().extend(label_selector); + /// ``` + pub fn extend(mut self, exprs: impl IntoIterator) -> Self { + self.0.extend(exprs); + self + } +} + +impl Matcher for LabelSelector { + fn matches(&self, labels: &Map) -> bool { + let selector: Selector = self.clone().into(); + selector.matches(labels) + } +} + +impl Matcher for Selector { + // Perform a match check on the resource labels + fn matches(&self, labels: &Map) -> bool { for expr in self.0.iter() { if !expr.matches(labels) { return false; @@ -64,32 +174,7 @@ impl Selector { } } -// === Expression === - -impl Expression { - /// Perform conversion to string - pub fn to_string(&self) -> String { - match self { - Expression::In(key, values) => { - format!( - "{key} in ({})", - values.into_iter().cloned().collect::>().join(",") - ) - } - Expression::NotIn(key, values) => { - format!( - "{key} notin ({})", - values.into_iter().cloned().collect::>().join(",") - ) - } - Expression::Equal(key, value) => format!("{key}={value}"), - Expression::NotEqual(key, value) => format!("{key}!={value}"), - Expression::Exists(key) => format!("{key}"), - Expression::DoesNotExist(key) => format!("!{key}"), - Expression::Invalid => "".into(), - } - } - +impl Matcher for Expression { fn matches(&self, labels: &Map) -> bool { match self { Expression::In(key, values) => match labels.get(key) { @@ -109,8 +194,59 @@ impl Expression { } } +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}"), + Expression::Invalid => Ok(()), + } + } +} + +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; -// convenience conversions for Selector + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} impl FromIterator<(String, String)> for Selector { fn from_iter>(iter: T) -> Self { @@ -148,7 +284,7 @@ impl From for Selector { }; let mut equality: Selector = value .match_labels - .and_then(|labels| Some(labels.into_iter().collect())) + .map(|labels| labels.into_iter().collect()) .unwrap_or_default(); equality.0.extend(expressions); equality @@ -393,7 +529,7 @@ mod tests { } #[test] - fn test_to_selector_string() { + 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()), @@ -402,7 +538,7 @@ mod tests { Expression::Exists("foo".into()), Expression::DoesNotExist("foo".into()), ]) - .to_selector_string(); + .to_string(); assert_eq!( selector, diff --git a/kube-core/src/lib.rs b/kube-core/src/lib.rs index b9b2a7a2e..0b169e264 100644 --- a/kube-core/src/lib.rs +++ b/kube-core/src/lib.rs @@ -52,6 +52,8 @@ pub use resource::{ pub mod response; pub use response::Status; +pub use labels::{Expression, Matcher, 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 f70e31596..7603e947e 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::{labels, request::Error}; +use crate::{request::Error, Selector}; use serde::Serialize; /// Controls how the resource version parameter is applied for list calls @@ -168,20 +168,20 @@ impl ListParams { /// Configure typed label selectors /// - /// Configure typed selectors from [`Selector`](crate::labels::Selector) and [`Expression`](crate::label::Expression) lists. + /// Configure typed selectors from [`Selector`](crate::Selector) and [`Expression`](crate::Expression) lists. /// /// ``` /// use kube::api::ListParams; - /// use kube_core::labels::{Expression, Selector}; + /// use kube_core::{Expression, Selector}; /// 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(&selector); /// // Alternatively the raw LabelSelector is accepted - /// let lp = ListParams::default().labels_from(LabelSelector::default().into()); + /// let lp = ListParams::default().labels_from(&LabelSelector::default().into()); ///``` #[must_use] - pub fn labels_from(mut self, selector: labels::Selector) -> Self { - self.label_selector = Some(selector.to_selector_string()); + pub fn labels_from(mut self, selector: &Selector) -> Self { + self.label_selector = Some(selector.to_string()); self } @@ -448,6 +448,25 @@ 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}; + /// 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); + /// // Alternatively the raw LabelSelector is accepted + /// let wp = WatchParams::default().labels_from(&LabelSelector::default().into()); + ///``` + #[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. diff --git a/kube-runtime/src/watcher.rs b/kube-runtime/src/watcher.rs index e259e154a..5b8826aa2 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,25 @@ 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}; + /// 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); + /// // Alternatively the raw LabelSelector is accepted + /// let cfg = Config::default().labels_from(&LabelSelector::default().into()); + ///``` + #[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..3fae1db73 100644 --- a/kube/src/lib.rs +++ b/kube/src/lib.rs @@ -199,7 +199,10 @@ pub mod prelude { #[cfg(feature = "unstable-client")] pub use crate::client::scope::NamespacedRef; #[allow(unreachable_pub)] pub use crate::core::PartialObjectMetaExt as _; - pub use crate::{core::crd::CustomResourceExt as _, Resource as _, ResourceExt as _}; + pub use crate::{ + core::{crd::CustomResourceExt as _, SelectorExt as _}, + Resource as _, ResourceExt as _, + }; #[cfg(feature = "runtime")] pub use crate::runtime::utils::WatchStreamExt as _; }