From 6b9e01be9187f4d26c79bd45d3dcea087d4ed4bb Mon Sep 17 00:00:00 2001 From: Danil-Grigorev <danil.grigorev@suse.com> Date: Wed, 17 Jul 2024 16:55:17 +0200 Subject: [PATCH] Implement SelectorExt and a Matcher trait - Add SelectorExt to the prelude - Remove Map in favor of more explicit BTreeMap<String, String> - Add exmples of usage Signed-off-by: Danil-Grigorev <danil.grigorev@suse.com> --- kube-core/src/labels.rs | 229 +++++++++++++++++++++++++++++------- kube-core/src/lib.rs | 2 + kube-core/src/params.rs | 35 ++++-- kube-runtime/src/watcher.rs | 22 +++- kube/src/lib.rs | 5 +- 5 files changed, 239 insertions(+), 54 deletions(-) diff --git a/kube-core/src/labels.rs b/kube-core/src/labels.rs index 449c118ba..03754d176 100644 --- a/kube-core/src/labels.rs +++ b/kube-core/src/labels.rs @@ -1,25 +1,122 @@ #![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<String, String>; type Expressions = Vec<Expression>; +/// Extensions to [`ResourceExt`](crate::ResourceExt) +/// Helper methods for resource selection based on provided Selector +pub trait SelectorExt { + fn selector_map(&self) -> &BTreeMap<String, String>; + + /// 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<R: ResourceExt> SelectorExt for R { + fn selector_map(&self) -> &BTreeMap<String, String> { + self.labels() + } +} + +/// Matcher trait for implementing alternalive Selectors +pub trait Matcher { + // Perform a match check on the resource labels + fn matches(&self, labels: &BTreeMap<String, String>) -> 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<String>), + + /// 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<String>), + + /// 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, } @@ -34,27 +131,43 @@ impl Selector { } /// Create a selector from a map of key=value label matches - fn from_map(map: Map) -> Self { + fn from_map(map: BTreeMap<String, String>) -> Self { 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<String> = 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<Item = Expression>) -> Self { + self.0.extend(exprs); + self + } +} + +impl Matcher for LabelSelector { + fn matches(&self, labels: &BTreeMap<String, String>) -> 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: &BTreeMap<String, String>) -> bool { for expr in self.0.iter() { if !expr.matches(labels) { return false; @@ -64,33 +177,8 @@ 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::<Vec<_>>().join(",") - ) - } - Expression::NotIn(key, values) => { - format!( - "{key} notin ({})", - values.into_iter().cloned().collect::<Vec<_>>().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(), - } - } - - fn matches(&self, labels: &Map) -> bool { +impl Matcher for Expression { + fn matches(&self, labels: &BTreeMap<String, String>) -> bool { match self { Expression::In(key, values) => match labels.get(key) { Some(v) => values.contains(v), @@ -109,8 +197,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::<Vec<_>>().join(",") + ) + } + Expression::NotIn(key, values) => { + write!( + f, + "{key} notin ({})", + values.iter().cloned().collect::<Vec<_>>().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<String> = self.0.iter().map(|e| e.to_string()).collect(); + write!(f, "{}", selectors.join(",")) + } +} +// convenience conversions for Selector and Expression -// convenience conversions for Selector +impl IntoIterator for Expression { + type IntoIter = IntoIter<Self::Item>; + type Item = Self; + + fn into_iter(self) -> Self::IntoIter { + Some(self).into_iter() + } +} + +impl IntoIterator for Selector { + type IntoIter = std::vec::IntoIter<Self::Item>; + type Item = Expression; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} impl FromIterator<(String, String)> for Selector { fn from_iter<T: IntoIterator<Item = (String, String)>>(iter: T) -> Self { @@ -148,7 +287,7 @@ impl From<LabelSelector> 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 +532,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 +541,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..116cb57b7 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,21 @@ 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); + /// let lp = ListParams::default().labels_from(&Expression::Exists("foo".into()).into()); /// // 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 +449,26 @@ 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); + /// let wp = WatchParams::default().labels_from(&Expression::Exists("foo".into()).into()); + /// // 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..5329b9e8e 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,26 @@ 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); + /// let cfg = Config::default().labels_from(&Expression::Exists("foo".into()).into()); + /// // 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 _; }