Skip to main content

solti_model/domain/selector/
runner.rs

1//! # Runner selector.
2//!
3//! [`RunnerSelector`] matches runners by label equality and expression-based requirements.
4
5use serde::{Deserialize, Serialize};
6
7use super::{SelectorOperator, SelectorRequirement};
8use crate::Labels;
9
10/// Label selector for runner routing.
11///
12/// ```text
13///  TaskSpec
14///  ┌────────────────────────────────────────────────────────┐
15///  │  runner_selector:                                      │
16///  │    match_labels:      { "zone": "eu" }                 │
17///  │    match_expressions: [ {key:"gpu", op:Exists} ]       │
18///  └──────────────────────────┬─────────────────────────────┘
19///                             │  ALL requirements ANDed
20///                             ▼
21///  RunnerRouter::pick()
22///  ┌────────────────────────────────────────────────────────┐
23///  │  Runner A  labels: {"zone":"us","gpu":"a100"}  ✗ skip  │
24///  │  Runner B  labels: {"zone":"eu","gpu":"h100"}  ✓ match │
25///  │  Runner C  labels: {"zone":"eu"}               ✗ skip  │
26///  └────────────────────────────────────────────────────────┘
27/// ```
28///
29/// Both `match_labels` and `match_expressions` are ANDed together.
30/// An empty selector matches every runner.
31///
32/// ## Also
33///
34/// - [`SelectorRequirement`] individual expression-based requirement.
35/// - [`SelectorOperator`] set operators (`In`, `NotIn`, `Exists`, `DoesNotExist`).
36/// - [`TaskSpec`](crate::TaskSpec) carries optional `runner_selector`.
37#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct RunnerSelector {
40    /// Exact key=value pairs: sugar for `In` with a single value.
41    #[serde(default, skip_serializing_if = "Labels::is_empty")]
42    pub match_labels: Labels,
43
44    /// Set-based requirements, ANDed with `match_labels`.
45    #[serde(default, skip_serializing_if = "Vec::is_empty")]
46    pub match_expressions: Vec<SelectorRequirement>,
47}
48
49impl RunnerSelector {
50    /// Empty selector (matches everything).
51    #[inline]
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Selector from exact key=value pairs only.
57    #[inline]
58    pub fn from_labels(labels: Labels) -> Self {
59        Self {
60            match_labels: labels,
61            match_expressions: vec![],
62        }
63    }
64
65    /// Selector from expressions only.
66    #[inline]
67    pub fn from_expressions(expr: Vec<SelectorRequirement>) -> Self {
68        Self {
69            match_labels: Labels::new(),
70            match_expressions: expr,
71        }
72    }
73
74    /// Returns `true` if the selector has no requirements (matches everything).
75    #[inline]
76    pub fn is_empty(&self) -> bool {
77        self.match_labels.is_empty() && self.match_expressions.is_empty()
78    }
79
80    /// Check whether `labels` satisfy **all** requirements of this selector.
81    ///
82    /// - Each `match_labels` entry requires an exact key=value match.
83    /// - Each `match_expressions` entry is evaluated per its operator.
84    /// - All requirements are ANDed.
85    pub fn matches(&self, labels: &Labels) -> bool {
86        for (key, expected) in &self.match_labels {
87            match labels.get(key) {
88                Some(actual) if actual == expected => {}
89                _ => return false,
90            }
91        }
92
93        for req in &self.match_expressions {
94            let value = labels.get(&req.key);
95            let ok = match req.operator {
96                SelectorOperator::In => match value {
97                    Some(v) => req.values.iter().any(|x| x == v),
98                    None => false,
99                },
100                SelectorOperator::NotIn => match value {
101                    Some(v) => !req.values.iter().any(|x| x == v),
102                    None => true,
103                },
104                SelectorOperator::Exists => value.is_some(),
105                SelectorOperator::DoesNotExist => value.is_none(),
106            };
107            if !ok {
108                return false;
109            }
110        }
111        true
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    fn labels(pairs: &[(&str, &str)]) -> Labels {
120        let mut l = Labels::new();
121        for (k, v) in pairs {
122            l.insert(*k, *v);
123        }
124        l
125    }
126
127    fn labels_of(pairs: &[(&str, &str)]) -> Labels {
128        labels(pairs)
129    }
130
131    #[test]
132    fn empty_selector_matches_everything() {
133        let sel = RunnerSelector::new();
134        assert!(sel.matches(&labels(&[])));
135        assert!(sel.matches(&labels(&[("a", "b")])));
136    }
137
138    #[test]
139    fn match_labels_exact_hit() {
140        let sel = RunnerSelector::from_labels(labels_of(&[("zone", "eu")]));
141        assert!(sel.matches(&labels(&[("zone", "eu"), ("extra", "x")])));
142    }
143
144    #[test]
145    fn match_labels_value_mismatch() {
146        let sel = RunnerSelector::from_labels(labels_of(&[("zone", "eu")]));
147        assert!(!sel.matches(&labels(&[("zone", "us")])));
148    }
149
150    #[test]
151    fn match_labels_key_missing() {
152        let sel = RunnerSelector::from_labels(labels_of(&[("zone", "eu")]));
153        assert!(!sel.matches(&labels(&[])));
154    }
155
156    #[test]
157    fn expr_in_matches() {
158        let sel = RunnerSelector::from_expressions(vec![SelectorRequirement::r#in(
159            "gpu",
160            vec!["a100".into(), "h100".into()],
161        )]);
162        assert!(sel.matches(&labels(&[("gpu", "a100")])));
163        assert!(sel.matches(&labels(&[("gpu", "h100")])));
164        assert!(!sel.matches(&labels(&[("gpu", "t4")])));
165        assert!(!sel.matches(&labels(&[])));
166    }
167
168    #[test]
169    fn expr_not_in_matches() {
170        let sel = RunnerSelector::from_expressions(vec![SelectorRequirement::not_in(
171            "tier",
172            vec!["dev".into()],
173        )]);
174        assert!(sel.matches(&labels(&[("tier", "prod")])));
175        assert!(!sel.matches(&labels(&[("tier", "dev")])));
176        assert!(sel.matches(&labels(&[])));
177    }
178
179    #[test]
180    fn expr_exists_matches() {
181        let sel = RunnerSelector::from_expressions(vec![SelectorRequirement::exists("gpu")]);
182        assert!(sel.matches(&labels(&[("gpu", "any")])));
183        assert!(!sel.matches(&labels(&[])));
184    }
185
186    #[test]
187    fn expr_does_not_exist_matches() {
188        let sel =
189            RunnerSelector::from_expressions(vec![SelectorRequirement::does_not_exist("tainted")]);
190        assert!(sel.matches(&labels(&[])));
191        assert!(!sel.matches(&labels(&[("tainted", "true")])));
192    }
193
194    #[test]
195    fn labels_and_expressions_anded() {
196        let sel = RunnerSelector {
197            match_labels: labels_of(&[("zone", "eu")]),
198            match_expressions: vec![SelectorRequirement::exists("gpu")],
199        };
200        assert!(sel.matches(&labels(&[("zone", "eu"), ("gpu", "a100")])));
201        assert!(!sel.matches(&labels(&[("zone", "us"), ("gpu", "a100")])));
202        assert!(!sel.matches(&labels(&[("zone", "eu")])));
203    }
204
205    #[test]
206    fn multiple_expressions_anded() {
207        let sel = RunnerSelector::from_expressions(vec![
208            SelectorRequirement::r#in("tier", vec!["prod".into(), "staging".into()]),
209            SelectorRequirement::does_not_exist("tainted"),
210        ]);
211        assert!(sel.matches(&labels(&[("tier", "prod")])));
212        assert!(!sel.matches(&labels(&[("tier", "prod"), ("tainted", "true")])));
213        assert!(!sel.matches(&labels(&[("tier", "dev")])));
214    }
215
216    #[test]
217    fn serde_roundtrip() {
218        let sel = RunnerSelector {
219            match_labels: labels_of(&[("zone", "eu")]),
220            match_expressions: vec![SelectorRequirement::exists("gpu")],
221        };
222        let json = serde_json::to_string_pretty(&sel).unwrap();
223        let back: RunnerSelector = serde_json::from_str(&json).unwrap();
224        assert_eq!(back, sel);
225    }
226
227    #[test]
228    fn serde_empty_selector() {
229        let sel = RunnerSelector::new();
230        let json = serde_json::to_string(&sel).unwrap();
231        assert_eq!(json, "{}");
232        let back: RunnerSelector = serde_json::from_str(&json).unwrap();
233        assert_eq!(back, sel);
234    }
235
236    #[test]
237    fn is_empty() {
238        assert!(RunnerSelector::new().is_empty());
239        assert!(!RunnerSelector::from_labels(labels_of(&[("k", "v")])).is_empty());
240    }
241}