1use arc_swap::ArcSwap;
2use regex::Regex;
3use std::sync::Arc;
4
5use crate::types::{AttrValue, Resource};
6
7pub trait Labeler: Send + Sync {
13 fn applies_to(&self, kind: &str) -> bool;
17
18 fn apply(&self, res: &mut Resource);
20}
21
22#[allow(dead_code)]
24#[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 #[allow(dead_code)]
44 pub fn new(
45 kind: impl Into<String>,
46 field: impl Into<String>,
47 output: impl Into<String>,
48 table: Vec<(String, Regex)>,
49 ) -> Self {
50 Self {
51 kind: kind.into(),
52 field: field.into(),
53 output: output.into(),
54 table,
55 }
56 }
57}
58
59impl Labeler for RegexLabeler {
60 fn applies_to(&self, kind: &str) -> bool {
61 self.kind == kind
62 }
63
64 fn apply(&self, res: &mut Resource) {
65 let Some(AttrValue::String(value)) = res.attrs().get(&self.field).cloned() else {
66 return;
67 };
68 let mut out: Vec<AttrValue> = Vec::new();
69 for (label, re) in &self.table {
70 if re.is_match(&value) {
71 out.push(AttrValue::String(label.clone()));
72 }
73 }
74 if !out.is_empty() {
75 match res.attrs().get_mut(&self.output) {
76 Some(AttrValue::Set(existing)) => existing.extend(out),
77 _ => {
78 res.attrs().insert(self.output.clone(), AttrValue::Set(out));
79 }
80 }
81 }
82 }
83}
84
85pub struct LabelRegistry {
89 inner: ArcSwap<Vec<Arc<dyn Labeler>>>,
90}
91impl LabelRegistry {
92 pub fn apply(&self, res: &mut Resource) {
97 let snapshot = self.inner.load();
98 let kind_owned = res.kind().to_owned();
99 for l in snapshot.iter() {
100 if l.applies_to(&kind_owned) {
101 l.apply(res);
102 }
103 }
104 }
105
106 pub fn reload(&self, labelers: Vec<Arc<dyn Labeler>>) {
111 self.inner.store(Arc::new(labelers));
112 }
113}
114
115pub struct LabelRegistryBuilder {
137 labelers: Vec<Arc<dyn Labeler>>,
138}
139
140impl LabelRegistryBuilder {
141 pub fn new() -> Self {
143 Self {
144 labelers: Vec::new(),
145 }
146 }
147
148 pub fn add_labeler(mut self, labeler: Arc<dyn Labeler>) -> Self {
152 self.labelers.push(labeler);
153 self
154 }
155
156 pub fn build(self) -> LabelRegistry {
161 LabelRegistry {
162 inner: ArcSwap::from_pointee(self.labelers),
163 }
164 }
165}
166
167impl Default for LabelRegistryBuilder {
168 fn default() -> Self {
169 Self::new()
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use std::collections::BTreeSet;
177 use yare::parameterized;
178
179 fn compile(rules: Vec<(&str, &str)>) -> Vec<(String, Regex)> {
180 rules
181 .into_iter()
182 .map(|(l, p)| (l.to_string(), Regex::new(p).unwrap()))
183 .collect()
184 }
185
186 fn get_label_strings(res: &mut Resource, key: &str) -> BTreeSet<String> {
187 match res.attrs().get(key) {
188 Some(AttrValue::Set(v)) => v
189 .iter()
190 .filter_map(|a| {
191 if let AttrValue::String(s) = a {
192 Some(s.clone())
193 } else {
194 None
195 }
196 })
197 .collect(),
198 _ => BTreeSet::new(),
199 }
200 }
201
202 #[parameterized(
203 simple_match = {
204 "Host", "name", "nameLabels",
205 vec![("prod", r"(^|\.)prod\.example\.com$")],
206 "db12.prod.example.com",
207 &["prod"]
208 },
209 no_match = {
210 "Host", "name", "nameLabels",
211 vec![("corp", r"(^|\.)corp\.example\.com$")],
212 "web.dev.example.com",
213 &[]
214 },
215 multi_match = {
216 "Host", "name", "nameLabels",
217 vec![("prod", r"(^|\.)prod\."), ("db", r"(^|\.)db\d+\.")],
218 "db42.prod.example.com",
219 &["db","prod"]
220 }
221 )]
222 fn regex_labeler_apply_basic(
223 kind: &str,
224 field: &str,
225 output: &str,
226 rules: Vec<(&str, &str)>,
227 input: &str,
228 expected: &[&str],
229 ) {
230 let labeler = RegexLabeler::new(kind, field, output, compile(rules));
231
232 let mut res = Resource::new(kind, input);
233 res.attrs()
234 .insert(field.to_string(), AttrValue::String(input.to_string()));
235
236 labeler.apply(&mut res);
237
238 let got = get_label_strings(&mut res, output);
239 let want: BTreeSet<String> = expected.iter().map(|s| s.to_string()).collect();
240 assert_eq!(got, want);
241 }
242
243 #[test]
244 fn regex_labeler_missing_input_field_is_noop() {
245 let labeler = RegexLabeler::new(
246 "Host",
247 "name",
248 "nameLabels",
249 compile(vec![("prod", r"(^|\.)prod\.")]),
250 );
251
252 let mut res = Resource::new("Host", "db99.prod.example.com");
253 labeler.apply(&mut res);
256 assert!(res.attrs().get("nameLabels").is_none());
257 }
258
259 #[test]
260 fn regex_labeler_appends_to_existing_set() {
261 let labeler = RegexLabeler::new(
262 "Host",
263 "name",
264 "nameLabels",
265 compile(vec![("prod", r"(^|\.)prod\."), ("db", r"(^|\.)db\d+\.")]),
266 );
267
268 let mut res = Resource::new("Host", "db99.prod.example.com");
269 res.attrs().insert(
270 "name".into(),
271 AttrValue::String("db99.prod.example.com".into()),
272 );
273 res.attrs().insert(
274 "nameLabels".into(),
275 AttrValue::Set(vec![AttrValue::String("pre".into())]),
276 );
277
278 labeler.apply(&mut res);
279
280 let labels = get_label_strings(&mut res, "nameLabels");
281 assert!(labels.contains("pre"));
282 assert!(labels.contains("prod"));
283 assert!(labels.contains("db"));
284 }
285}