bindy/selector.rs
1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! Label selector matching utilities.
5//!
6//! This module provides helper functions for matching Kubernetes label selectors
7//! against resource labels. It supports both `matchLabels` and `matchExpressions`
8//! as defined in the Kubernetes API.
9//!
10//! # Architecture
11//!
12//! The label selector watch pattern uses kube-rs's reflector/store to maintain
13//! an in-memory cache of resources. When a resource changes, watch mappers
14//! synchronously query these caches to find related resources using label selectors.
15//!
16//! # Example
17//!
18//! ```rust,no_run
19//! use std::collections::BTreeMap;
20//! use bindy::crd::LabelSelector;
21//! use bindy::selector::matches_selector;
22//!
23//! # fn example() {
24//! let mut labels = BTreeMap::new();
25//! labels.insert("app".to_string(), "web".to_string());
26//!
27//! let mut match_labels = BTreeMap::new();
28//! match_labels.insert("app".to_string(), "web".to_string());
29//!
30//! let selector = LabelSelector {
31//! match_labels: Some(match_labels),
32//! match_expressions: None,
33//! };
34//!
35//! assert!(matches_selector(&selector, &labels));
36//! # }
37//! ```
38
39use crate::crd::{LabelSelector, LabelSelectorRequirement};
40use std::collections::BTreeMap;
41
42/// Check if a set of labels matches a label selector.
43///
44/// This function implements the Kubernetes label selector matching logic:
45/// - All `matchLabels` entries must be present with exact values
46/// - All `matchExpressions` requirements must be satisfied
47/// - An empty selector matches everything
48/// - A selector with no matchLabels and no matchExpressions matches everything
49///
50/// # Arguments
51/// * `selector` - The label selector to evaluate
52/// * `labels` - The labels to match against
53///
54/// # Returns
55/// `true` if the labels match the selector, `false` otherwise
56///
57/// # Examples
58/// ```
59/// use std::collections::BTreeMap;
60/// use bindy::crd::LabelSelector;
61/// use bindy::selector::matches_selector;
62///
63/// let mut labels = BTreeMap::new();
64/// labels.insert("app".to_string(), "web".to_string());
65/// labels.insert("env".to_string(), "prod".to_string());
66///
67/// let mut match_labels = BTreeMap::new();
68/// match_labels.insert("app".to_string(), "web".to_string());
69///
70/// let selector = LabelSelector {
71/// match_labels: Some(match_labels),
72/// match_expressions: None,
73/// };
74///
75/// assert!(matches_selector(&selector, &labels));
76/// ```
77#[must_use]
78pub fn matches_selector(selector: &LabelSelector, labels: &BTreeMap<String, String>) -> bool {
79 // Check matchLabels
80 if let Some(match_labels) = &selector.match_labels {
81 for (key, value) in match_labels {
82 if labels.get(key) != Some(value) {
83 return false;
84 }
85 }
86 }
87
88 // Check matchExpressions
89 if let Some(match_expressions) = &selector.match_expressions {
90 for expr in match_expressions {
91 if !matches_expression(expr, labels) {
92 return false;
93 }
94 }
95 }
96
97 true
98}
99
100/// Check if a set of labels matches a single label selector requirement.
101///
102/// Implements the four Kubernetes label selector operators:
103/// - `In`: Label value must be in the provided set
104/// - `NotIn`: Label value must not be in the provided set
105/// - `Exists`: Label key must be present (value doesn't matter)
106/// - `DoesNotExist`: Label key must not be present
107///
108/// # Arguments
109/// * `expr` - The label selector requirement to evaluate
110/// * `labels` - The labels to match against
111///
112/// # Returns
113/// `true` if the labels satisfy the requirement, `false` otherwise
114fn matches_expression(expr: &LabelSelectorRequirement, labels: &BTreeMap<String, String>) -> bool {
115 let key = &expr.key;
116 let values = expr.values.as_deref().unwrap_or(&[]);
117
118 match expr.operator.as_str() {
119 "In" => {
120 // Label must exist and value must be in the set
121 labels.get(key).is_some_and(|v| values.contains(v))
122 }
123 "NotIn" => {
124 // If label doesn't exist, it passes
125 // If label exists, value must not be in the set
126 labels.get(key).is_none_or(|v| !values.contains(v))
127 }
128 "Exists" => {
129 // Label key must be present
130 labels.contains_key(key)
131 }
132 "DoesNotExist" => {
133 // Label key must not be present
134 !labels.contains_key(key)
135 }
136 _ => {
137 // Unknown operator - fail closed
138 tracing::warn!("Unknown label selector operator: {}", expr.operator);
139 false
140 }
141 }
142}
143
144#[cfg(test)]
145#[path = "selector_tests.rs"]
146mod selector_tests;