1use arc_swap::ArcSwap;
2use regex::Regex;
3use std::sync::Arc;
4
5use crate::types::{AttrValue, Resource};
6
7pub trait Labeler: Send + Sync {
14 fn applies_to(&self, kind: &str) -> bool;
18
19 fn apply(&self, res: &mut Resource);
21}
22
23#[derive(Debug, Clone)]
25pub struct RegexLabeler {
26 kind: String,
28 field: String,
30 output: String,
32 table: Vec<(String, Regex)>,
34}
35
36impl RegexLabeler {
37 pub fn new(
51 kind: impl Into<String>,
52 field: impl Into<String>,
53 output: impl Into<String>,
54 table: Vec<(String, Regex)>,
55 ) -> Self {
56 Self {
57 kind: kind.into(),
58 field: field.into(),
59 output: output.into(),
60 table,
61 }
62 }
63}
64
65impl Labeler for RegexLabeler {
66 fn applies_to(&self, kind: &str) -> bool {
67 self.kind == kind
68 }
69
70 fn apply(&self, res: &mut Resource) {
71 let Some(AttrValue::String(value)) = res.attributes().get(&self.field) else {
72 res.attrs().remove(&self.output);
75 return;
76 };
77 let out = self
78 .table
79 .iter()
80 .filter(|(_, re)| re.is_match(value))
81 .map(|(label, _)| AttrValue::String(label.clone()))
82 .collect();
83
84 res.attrs().insert(self.output.clone(), AttrValue::Set(out));
88 }
89}
90
91pub struct LabelRegistry {
95 inner: ArcSwap<Vec<Arc<dyn Labeler>>>,
96}
97impl LabelRegistry {
98 pub(crate) fn apply_to_clone_if_applicable(&self, res: &Resource) -> Option<Resource> {
105 let snapshot = self.inner.load();
106 let first_match = snapshot
107 .iter()
108 .position(|labeler| labeler.applies_to(res.kind()))?;
109
110 let mut labelled = res.clone();
111 snapshot[first_match].apply(&mut labelled);
112 for labeler in &snapshot[first_match + 1..] {
113 if labeler.applies_to(labelled.kind()) {
114 labeler.apply(&mut labelled);
115 }
116 }
117 Some(labelled)
118 }
119
120 pub fn apply(&self, res: &mut Resource) {
125 let snapshot = self.inner.load();
126 for l in snapshot.iter() {
127 if l.applies_to(res.kind()) {
128 l.apply(res);
129 }
130 }
131 }
132
133 pub fn reload(&self, labelers: Vec<Arc<dyn Labeler>>) {
138 self.inner.store(Arc::new(labelers));
139 }
140}
141
142pub struct LabelRegistryBuilder {
164 labelers: Vec<Arc<dyn Labeler>>,
165}
166
167impl LabelRegistryBuilder {
168 pub fn new() -> Self {
170 Self {
171 labelers: Vec::new(),
172 }
173 }
174
175 pub fn add_labeler(mut self, labeler: Arc<dyn Labeler>) -> Self {
179 self.labelers.push(labeler);
180 self
181 }
182
183 pub fn build(self) -> LabelRegistry {
188 LabelRegistry {
189 inner: ArcSwap::from_pointee(self.labelers),
190 }
191 }
192}
193
194impl Default for LabelRegistryBuilder {
195 fn default() -> Self {
196 Self::new()
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use std::collections::BTreeSet;
204 use yare::parameterized;
205
206 fn compile(rules: Vec<(&str, &str)>) -> Vec<(String, Regex)> {
207 rules
208 .into_iter()
209 .map(|(l, p)| (l.to_string(), Regex::new(p).unwrap()))
210 .collect()
211 }
212
213 fn get_label_strings(res: &mut Resource, key: &str) -> BTreeSet<String> {
214 match res.attrs().get(key) {
215 Some(AttrValue::Set(v)) => v
216 .iter()
217 .filter_map(|a| {
218 if let AttrValue::String(s) = a {
219 Some(s.clone())
220 } else {
221 None
222 }
223 })
224 .collect(),
225 _ => BTreeSet::new(),
226 }
227 }
228
229 #[parameterized(
230 simple_match = {
231 "Host", "name", "nameLabels",
232 vec![("prod", r"(^|\.)prod\.example\.com$")],
233 "db12.prod.example.com",
234 &["prod"]
235 },
236 no_match = {
237 "Host", "name", "nameLabels",
238 vec![("corp", r"(^|\.)corp\.example\.com$")],
239 "web.dev.example.com",
240 &[]
241 },
242 multi_match = {
243 "Host", "name", "nameLabels",
244 vec![("prod", r"(^|\.)prod\."), ("db", r"(^|\.)db\d+\.")],
245 "db42.prod.example.com",
246 &["db","prod"]
247 }
248 )]
249 fn regex_labeler_apply_basic(
250 kind: &str,
251 field: &str,
252 output: &str,
253 rules: Vec<(&str, &str)>,
254 input: &str,
255 expected: &[&str],
256 ) {
257 let labeler = RegexLabeler::new(kind, field, output, compile(rules));
258
259 let mut res = Resource::new(kind, input);
260 res.attrs()
261 .insert(field.to_string(), AttrValue::String(input.to_string()));
262
263 labeler.apply(&mut res);
264
265 let got = get_label_strings(&mut res, output);
266 let want: BTreeSet<String> = expected.iter().map(|s| s.to_string()).collect();
267 assert_eq!(got, want);
268 }
269
270 #[test]
271 fn regex_labeler_missing_input_field_is_noop() {
272 let labeler = RegexLabeler::new(
273 "Host",
274 "name",
275 "nameLabels",
276 compile(vec![("prod", r"(^|\.)prod\.")]),
277 );
278
279 let mut res = Resource::new("Host", "db99.prod.example.com");
280 labeler.apply(&mut res);
283 assert!(res.attrs().get("nameLabels").is_none());
284 }
285
286 #[test]
287 fn regex_labeler_replaces_untrusted_existing_set() {
288 let labeler = RegexLabeler::new(
289 "Host",
290 "name",
291 "nameLabels",
292 compile(vec![("prod", r"(^|\.)prod\."), ("db", r"(^|\.)db\d+\.")]),
293 );
294
295 let mut res = Resource::new("Host", "db99.prod.example.com");
296 res.attrs().insert(
297 "name".into(),
298 AttrValue::String("db99.prod.example.com".into()),
299 );
300 res.attrs().insert(
301 "nameLabels".into(),
302 AttrValue::Set(vec![AttrValue::String("pre".into())]),
303 );
304
305 labeler.apply(&mut res);
306
307 let labels = get_label_strings(&mut res, "nameLabels");
308 assert!(!labels.contains("pre"));
309 assert!(labels.contains("prod"));
310 assert!(labels.contains("db"));
311 }
312
313 #[test]
314 fn regex_labeler_replaces_untrusted_set_when_no_rule_matches() {
315 let labeler = RegexLabeler::new(
316 "Host",
317 "name",
318 "nameLabels",
319 compile(vec![("prod", r"(^|\.)prod\.")]),
320 );
321 let mut res = Resource::new("Host", "public.example.com")
322 .with_attr("name", AttrValue::String("public.example.com".into()))
323 .with_attr(
324 "nameLabels",
325 AttrValue::Set(vec![AttrValue::String("prod".into())]),
326 );
327
328 labeler.apply(&mut res);
329
330 assert_eq!(
331 res.attributes().get("nameLabels"),
332 Some(&AttrValue::Set(Vec::new()))
333 );
334 }
335
336 #[test]
337 fn regex_labeler_removes_untrusted_output_when_input_is_missing() {
338 let labeler = RegexLabeler::new(
339 "Host",
340 "name",
341 "nameLabels",
342 compile(vec![("prod", r"(^|\.)prod\.")]),
343 );
344 let mut res = Resource::new("Host", "public.example.com").with_attr(
345 "nameLabels",
346 AttrValue::Set(vec![AttrValue::String("prod".into())]),
347 );
348
349 labeler.apply(&mut res);
350
351 assert!(!res.attributes().contains_key("nameLabels"));
352 }
353}