Skip to content

Commit

Permalink
Implement SelectorExt and a Matcher trait
Browse files Browse the repository at this point in the history
- Add SelectorExt to the prelude

Signed-off-by: Danil-Grigorev <[email protected]>
  • Loading branch information
Danil-Grigorev committed Jul 18, 2024
1 parent e056a77 commit 7bfb50d
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 51 deletions.
220 changes: 178 additions & 42 deletions kube-core/src/labels.rs
Original file line number Diff line number Diff line change
@@ -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<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) -> &Map;

/// 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) -> &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<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)")
/// ```
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,
}

Expand All @@ -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<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: &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;
Expand All @@ -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::<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(),
}
}

impl Matcher for Expression {
fn matches(&self, labels: &Map) -> bool {
match self {
Expression::In(key, values) => match labels.get(key) {
Expand All @@ -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::<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

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;

// convenience conversions for Selector
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 {
Expand Down Expand Up @@ -148,7 +284,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
Expand Down Expand Up @@ -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()),
Expand All @@ -402,7 +538,7 @@ mod tests {
Expression::Exists("foo".into()),
Expression::DoesNotExist("foo".into()),
])
.to_selector_string();
.to_string();

assert_eq!(
selector,
Expand Down
2 changes: 2 additions & 0 deletions kube-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
33 changes: 26 additions & 7 deletions kube-core/src/params.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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.
Expand Down
21 changes: 20 additions & 1 deletion kube-runtime/src/watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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`].
Expand Down
Loading

0 comments on commit 7bfb50d

Please sign in to comment.