solti_model/domain/selector/
runner.rs1use serde::{Deserialize, Serialize};
6
7use super::{SelectorOperator, SelectorRequirement};
8use crate::Labels;
9
10#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct RunnerSelector {
40 #[serde(default, skip_serializing_if = "Labels::is_empty")]
42 pub match_labels: Labels,
43
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
46 pub match_expressions: Vec<SelectorRequirement>,
47}
48
49impl RunnerSelector {
50 #[inline]
52 pub fn new() -> Self {
53 Self::default()
54 }
55
56 #[inline]
58 pub fn from_labels(labels: Labels) -> Self {
59 Self {
60 match_labels: labels,
61 match_expressions: vec![],
62 }
63 }
64
65 #[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 #[inline]
76 pub fn is_empty(&self) -> bool {
77 self.match_labels.is_empty() && self.match_expressions.is_empty()
78 }
79
80 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}