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

Label selector support #1539

Merged
merged 7 commits into from
Jul 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
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 <[email protected]>
  • Loading branch information
Danil-Grigorev committed Jul 18, 2024
commit 4fe0ab5a83f248b7ced067f775d03e86c13af095
230 changes: 185 additions & 45 deletions kube-core/src/labels.rs
Original file line number Diff line number Diff line change
@@ -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())

Check warning on line 36 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L35-L36

Added lines #L35 - L36 were not covered by tests
}
}

impl<R: ResourceExt> SelectorExt for R {
fn selector_map(&self) -> &BTreeMap<String, String> {
self.labels()

Check warning on line 42 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L41-L42

Added lines #L41 - L42 were not covered by tests
}
}

/// 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,
}

Expand All @@ -34,27 +131,44 @@
}

/// 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()

Check warning on line 140 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L139-L140

Added lines #L139 - L140 were not covered by tests
}

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;
///
/// let label_selector: Selector = LabelSelector::default().into();
/// let mut selector = Selector::default();
/// selector.extend(Expression::Equal("environment".into(), "production".into()));
/// selector.extend([Expression::Exists("bar".into()), Expression::Exists("foo".into())].into_iter());
/// selector.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)

Check warning on line 165 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L163-L165

Added lines #L163 - L165 were not covered by tests
}
}

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;
Expand All @@ -64,53 +178,79 @@
}
}

// === 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),
None => false,

Check warning on line 186 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L186

Added line #L186 was not covered by tests
},
Expression::NotIn(key, values) => match labels.get(key) {
Some(v) => !values.contains(v),
None => true,

Check warning on line 190 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L190

Added line #L190 was not covered by tests
},
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),
Expression::Invalid => false,

Check warning on line 196 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L196

Added line #L196 was not covered by tests
}
}
}

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(()),

Check warning on line 223 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L223

Added line #L223 was not covered by tests
}
}
}

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()

Check warning on line 242 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L241-L242

Added lines #L241 - L242 were not covered by tests
}
}

impl IntoIterator for Selector {
type IntoIter = std::vec::IntoIter<Self::Item>;
type Item = Expression;

fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()

Check warning on line 251 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L250-L251

Added lines #L250 - L251 were not covered by tests
}
}

impl FromIterator<(String, String)> for Selector {
fn from_iter<T: IntoIterator<Item = (String, String)>>(iter: T) -> Self {
Expand All @@ -123,7 +263,7 @@
Self::from_map(
iter.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),

Check warning on line 266 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L266

Added line #L266 was not covered by tests
)
}
}
Expand All @@ -135,8 +275,8 @@
}

impl From<Expression> for Selector {
fn from(value: Expression) -> Self {
Self(vec![value])

Check warning on line 279 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L278-L279

Added lines #L278 - L279 were not covered by tests
}
}

Expand All @@ -148,7 +288,7 @@
};
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 All @@ -162,15 +302,15 @@
match requirement.operator.as_str() {
"In" => match values {
Some(values) => Expression::In(key, values),
None => Expression::Invalid,

Check warning on line 305 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L305

Added line #L305 was not covered by tests
},
"NotIn" => match values {
Some(values) => Expression::NotIn(key, values),
None => Expression::Invalid,

Check warning on line 309 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L309

Added line #L309 was not covered by tests
},
"Exists" => Expression::Exists(key),
"DoesNotExist" => Expression::DoesNotExist(key),
_ => Expression::Invalid,

Check warning on line 313 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L313

Added line #L313 was not covered by tests
}
}
}
Expand All @@ -197,15 +337,15 @@
operator: "NotIn".into(),
values: Some(vec![value]),
}),
Expression::Exists(key) => expressions.push(LabelSelectorRequirement {
key,
operator: "Exists".into(),
values: None,

Check warning on line 343 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L340-L343

Added lines #L340 - L343 were not covered by tests
}),
Expression::DoesNotExist(key) => expressions.push(LabelSelectorRequirement {
key,
operator: "DoesNotExist".into(),
values: None,

Check warning on line 348 in kube-core/src/labels.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/labels.rs#L345-L348

Added lines #L345 - L348 were not covered by tests
}),
Expression::Invalid => (),
}
Expand Down Expand Up @@ -393,7 +533,7 @@
}

#[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 +542,7 @@
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
35 changes: 28 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,21 +168,22 @@

/// 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

Check warning on line 186 in kube-core/src/params.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/params.rs#L184-L186

Added lines #L184 - L186 were not covered by tests
}

/// Sets a result limit.
Expand Down Expand Up @@ -448,6 +449,26 @@
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

Check warning on line 469 in kube-core/src/params.rs

View check run for this annotation

Codecov / codecov/patch

kube-core/src/params.rs#L467-L469

Added lines #L467 - L469 were not covered by tests
}

/// Disables watch bookmarks to simplify watch handling
///
/// This is not recommended to use with production watchers as it can cause desyncs.
Expand Down
Loading
Loading