pharos_plugins/kubernetes/
setup.rs1use pharos_corefile::{Cursor, Error, Fallthrough, origins_from_args_or_server_block};
2use std::{
3 collections::{BTreeMap, BTreeSet, HashSet},
4 time::Duration,
5};
6
7#[derive(Debug)]
8pub struct Config {
9 pub zones: Vec<String>,
10 pub multicluster_zones: Vec<String>,
11 pub primary_zone_index: usize,
12 pub ttl: u32,
13 pub pod_mode: String,
14 pub endpoints: bool,
15 pub endpoints_list: Vec<String>,
16 pub tls: Option<[String; 3]>,
17 pub kubeconfig: Option<(String, Option<String>)>,
18 pub namespaces: HashSet<String>,
19 pub label_selector: Option<String>,
20 pub namespace_label_selector: Option<String>,
21 pub endpoint_pod_names: bool,
22 pub ignore_empty_service: bool,
23 pub zonal: bool,
24 pub api_qps: f32,
25 pub api_burst: usize,
26 pub api_max_inflight: usize,
27 pub startup_timeout: Duration,
28 pub fallthrough: Fallthrough,
29}
30
31impl Config {
32 pub fn client_config(&self) -> pharos_kubernetes::ClientConfig {
33 pharos_kubernetes::ClientConfig {
34 endpoint: self.endpoints_list.first().cloned(),
35 tls: self
36 .tls
37 .as_ref()
38 .map(|[cert, key, ca]| pharos_kubernetes::TlsConfig {
39 cert: cert.into(),
40 key: key.into(),
41 ca: ca.into(),
42 }),
43 kubeconfig: self
44 .kubeconfig
45 .as_ref()
46 .map(|(file, context)| (file.into(), context.clone())),
47 api_qps: self.api_qps,
48 api_burst: self.api_burst,
49 api_max_inflight: self.api_max_inflight,
50 }
51 }
52
53 pub fn watch_config(&self) -> pharos_kubernetes::WatchConfig {
54 let mut namespaces = self.namespaces.iter().cloned().collect::<Vec<_>>();
55 namespaces.sort();
56 pharos_kubernetes::WatchConfig {
57 namespaces,
58 label_selector: self.label_selector.clone(),
59 namespace_label_selector: self.namespace_label_selector.clone(),
60 pods_verified: self.pod_mode == "verified",
61 endpoints: self.endpoints,
62 zonal: self.zonal,
63 multicluster: !self.multicluster_zones.is_empty(),
64 }
65 }
66}
67
68pub fn parse(cursor: &mut Cursor<'_>, server_domains: &[&str]) -> Result<Config, Error> {
69 cursor.next();
70 let args = cursor.remaining_args();
71 let zones = origins_from_args_or_server_block(
72 &args.iter().map(String::as_str).collect::<Vec<_>>(),
73 server_domains,
74 );
75 let primary_zone_index = zones
76 .iter()
77 .position(|z| !is_reverse(z))
78 .ok_or_else(|| cursor.error("non-reverse zone name must be used"))?;
79 let mut config = Config {
80 zones,
81 multicluster_zones: Vec::new(),
82 primary_zone_index,
83 ttl: 5,
84 pod_mode: "disabled".into(),
85 endpoints: true,
86 endpoints_list: vec![],
87 tls: None,
88 kubeconfig: None,
89 namespaces: HashSet::new(),
90 label_selector: None,
91 namespace_label_selector: None,
92 endpoint_pod_names: false,
93 ignore_empty_service: false,
94 zonal: false,
95 api_qps: 0.0,
96 api_burst: 0,
97 api_max_inflight: 0,
98 startup_timeout: Duration::from_secs(5),
99 fallthrough: Fallthrough::default(),
100 };
101 while cursor.next_block() {
102 let option = cursor.val().to_owned();
103 let args = cursor.remaining_args();
104 match option.as_str() {
105 "endpoint_pod_names" | "noendpoints" | "zonal" => {
106 if !args.is_empty() {
107 return Err(cursor.arg_err());
108 }
109 match option.as_str() {
110 "endpoint_pod_names" => config.endpoint_pod_names = true,
111 "noendpoints" => config.endpoints = false,
112 _ => config.zonal = true,
113 }
114 }
115 "pods" => {
116 if args.len() != 1 {
117 return Err(cursor.arg_err());
118 }
119 if !matches!(args[0].as_str(), "disabled" | "verified" | "insecure") {
120 return Err(cursor.error(format!("wrong value for pods: {}", args[0])));
121 }
122 config.pod_mode = args[0].clone();
123 }
124 "namespaces" => {
125 if args.is_empty() {
126 return Err(cursor.arg_err());
127 }
128 config.namespaces.extend(args);
129 }
130 "endpoint" => {
131 if args.is_empty() {
132 return Err(cursor.arg_err());
133 }
134 config.endpoints_list = args;
135 }
136 "tls" => {
137 if args.len() != 3 {
138 return Err(cursor.arg_err());
139 }
140 config.tls = Some([args[0].clone(), args[1].clone(), args[2].clone()]);
141 }
142 "labels" | "namespace_labels" => {
143 if args.is_empty() {
144 return Err(cursor.arg_err());
145 }
146 let selector = args.join(" ");
147 let selector = parse_selector(&selector).ok_or_else(|| {
148 cursor.error(format!(
149 "unable to parse {option} selector value: '{selector}'"
150 ))
151 })?;
152 if option == "labels" {
153 config.label_selector = Some(selector);
154 } else {
155 config.namespace_label_selector = Some(selector);
156 }
157 }
158 "fallthrough" => config
159 .fallthrough
160 .set_domains_from_args(&args.iter().map(String::as_str).collect::<Vec<_>>()),
161 "ttl" => {
162 let value = args.first().ok_or_else(|| cursor.arg_err())?;
163 let ttl: i64 = value.parse().map_err(|_| cursor.error("invalid ttl"))?;
164 if !(0..=3600).contains(&ttl) {
165 return Err(cursor.error("ttl must be in range [0, 3600]"));
166 }
167 config.ttl = ttl as u32;
168 }
169 "ignore" => {
170 if let Some(value) = args.first() {
171 if value != "empty_service" {
172 return Err(
173 cursor.error(format!("unable to parse ignore value: '{value}'"))
174 );
175 }
176 config.ignore_empty_service = true;
177 }
178 }
179 "kubeconfig" => {
180 if !(1..=2).contains(&args.len()) {
181 return Err(cursor.arg_err());
182 }
183 config.kubeconfig = Some((args[0].clone(), args.get(1).cloned()));
184 }
185 "multicluster" => {
186 config.multicluster_zones = origins_from_args_or_server_block(
187 &args.iter().map(String::as_str).collect::<Vec<_>>(),
188 &[],
189 )
190 }
191 "startup_timeout" => {
192 let value = args.first().ok_or_else(|| cursor.arg_err())?;
193 config.startup_timeout = crate::health::parse_duration(value).ok_or_else(|| {
195 cursor.error(format!("failed to parse startup_timeout: {value}"))
196 })?;
197 }
198 "apiserver_qps" => {
199 if args.len() != 1 {
200 return Err(cursor.arg_err());
201 }
202 let value =
204 parse_float32(&args[0]).ok_or_else(|| cursor.error("invalid apiserver_qps"))?;
205 if value < 0.0 {
206 return Err(cursor.error("apiserver_qps must be >= 0"));
207 }
208 config.api_qps = value;
209 }
210 "apiserver_burst" | "apiserver_max_inflight" => {
211 if args.len() != 1 {
212 return Err(cursor.arg_err());
213 }
214 let value: i64 = args[0]
215 .parse()
216 .map_err(|_| cursor.error(format!("invalid {option}")))?;
217 if value < 0 {
218 return Err(cursor.error(format!("{option} must be >= 0")));
219 }
220 let value = usize::try_from(value)
221 .map_err(|_| cursor.error(format!("invalid {option}")))?;
222 if option == "apiserver_burst" {
223 config.api_burst = value;
224 } else {
225 config.api_max_inflight = value;
226 }
227 }
228 _ => return Err(cursor.error(format!("unknown property '{option}'"))),
229 }
230 }
231 if cursor.has_next() {
232 return Err(cursor.error("kubernetes: this plugin can only be used once per Server Block"));
233 }
234 if !config.namespaces.is_empty() && config.namespace_label_selector.is_some() {
235 return Err(cursor.error("namespaces and namespace_labels cannot both be set"));
236 }
237 for zone in &config.multicluster_zones {
238 if !config.zones.contains(zone) {
239 return Err(cursor.error(format!(
240 "is not authoritative for the multicluster zone {zone} (authoritative zones: {:?})",
241 config.zones
242 )));
243 }
244 }
245 if config.zonal && !config.endpoints {
246 return Err(cursor.error("zonal requires the endpoint cache; remove noendpoints"));
247 }
248 Ok(config)
249}
250
251fn parse_float32(input: &str) -> Option<f32> {
252 let (negative, body) = if let Some(body) = input.strip_prefix('-') {
253 (true, body)
254 } else {
255 (false, input.strip_prefix('+').unwrap_or(input))
256 };
257 if body.starts_with(['+', '-']) {
258 return None;
259 }
260 if body.eq_ignore_ascii_case("nan") {
261 return (body.len() == input.len()).then_some(f32::NAN);
262 }
263 if body.eq_ignore_ascii_case("inf") || body.eq_ignore_ascii_case("infinity") {
264 return Some(if negative {
265 f32::NEG_INFINITY
266 } else {
267 f32::INFINITY
268 });
269 }
270 let hex = body.starts_with("0x") || body.starts_with("0X");
271 let exponent = body.bytes().position(|b| matches!(b, b'p' | b'P'));
272 let bytes = body.as_bytes();
273 for (i, &byte) in bytes.iter().enumerate() {
274 if byte != b'_' {
275 continue;
276 }
277 let is_digit = |b: u8| {
278 if hex && exponent.is_none_or(|e| i < e) {
279 b.is_ascii_hexdigit()
280 } else {
281 b.is_ascii_digit()
282 }
283 };
284 if !(hex && i == 2 || i > 0 && is_digit(bytes[i - 1]))
285 || !bytes.get(i + 1).is_some_and(|&b| is_digit(b))
286 {
287 return None;
288 }
289 }
290 let cleaned = body.replace('_', "");
291 if hex {
292 return parse_hex_float32(&cleaned[2..], negative);
293 }
294 let value = cleaned.parse::<f32>().ok()?;
295 if !value.is_finite() {
296 return None;
297 }
298 Some(if negative { -value } else { value })
299}
300
301fn parse_hex_float32(input: &str, negative: bool) -> Option<f32> {
302 let (mantissa, exponent) = input.split_once(['p', 'P'])?;
303 let (exp_negative, exponent) = if let Some(rest) = exponent.strip_prefix('-') {
304 (true, rest)
305 } else {
306 (false, exponent.strip_prefix('+').unwrap_or(exponent))
307 };
308 if exponent.is_empty() || !exponent.bytes().all(|b| b.is_ascii_digit()) {
309 return None;
310 }
311 let exponent = exponent.bytes().fold(0i64, |n, b| {
312 n.saturating_mul(10).saturating_add(i64::from(b - b'0'))
313 });
314 let exponent = if exp_negative { -exponent } else { exponent };
315 let mut digits = Vec::new();
316 let mut point = None;
317 for byte in mantissa.bytes() {
318 if byte == b'.' && point.is_none() {
319 point = Some(digits.len());
320 } else {
321 digits.push((byte as char).to_digit(16)? as u8);
322 }
323 }
324 if digits.is_empty() {
325 return None;
326 }
327 let sign = if negative { 1 << 31 } else { 0 };
328 let Some(first) = digits.iter().position(|&digit| digit != 0) else {
329 return Some(f32::from_bits(sign));
330 };
331 let integer_digits = point.unwrap_or(digits.len());
332 let offset = (integer_digits as i64)
333 .saturating_sub(first as i64)
334 .saturating_sub(1)
335 .saturating_mul(4);
336 let mut exponent = exponent
337 .saturating_add(offset)
338 .saturating_add(i64::from(digits[first].ilog2()));
339 if exponent > 127 {
340 return None;
341 }
342 if exponent < -150 {
343 return Some(f32::from_bits(sign));
344 }
345 let precision = if exponent >= -126 {
347 24
348 } else {
349 (exponent + 150) as usize
350 };
351 let mut value = 0u32;
352 let mut count = 0usize;
353 let mut round = false;
354 let mut sticky = false;
355 for (i, &digit) in digits[first..].iter().enumerate() {
356 let high = if i == 0 { digit.ilog2() } else { 3 };
357 for bit in (0..=high).rev() {
358 let set = digit & (1 << bit) != 0;
359 if count < precision {
360 value = (value << 1) | u32::from(set);
361 } else if count == precision {
362 round = set;
363 } else {
364 sticky |= set;
365 }
366 count += 1;
367 }
368 }
369 if count < precision {
370 value <<= precision - count;
371 }
372 if round && (sticky || value & 1 != 0) {
373 value += 1;
374 }
375 if exponent >= -126 {
376 if value == 1 << 24 {
377 value >>= 1;
378 exponent += 1;
379 }
380 if exponent > 127 {
381 return None;
382 }
383 value = (((exponent + 127) as u32) << 23) | (value & 0x7f_ffff);
384 }
385 Some(f32::from_bits(sign | value))
386}
387
388fn is_reverse(zone: &str) -> bool {
389 let zone = zone.to_ascii_lowercase();
390 [".in-addr.arpa.", ".ip6.arpa."]
391 .iter()
392 .any(|suffix| zone.ends_with(suffix))
393}
394
395fn parse_selector(input: &str) -> Option<String> {
396 let mut tokens = Vec::new();
397 let mut chars = input.char_indices().peekable();
398 while let Some((start, ch)) = chars.next() {
399 if ch.is_whitespace() {
400 continue;
401 }
402 if "(),!=<>".contains(ch) {
403 let mut end = start + ch.len_utf8();
404 if matches!(ch, '!' | '=') && chars.peek().is_some_and(|(_, next)| *next == '=') {
405 let (index, next) = chars.next().unwrap();
406 end = index + next.len_utf8();
407 }
408 tokens.push(&input[start..end]);
409 } else {
410 let mut end = start + ch.len_utf8();
411 while let Some(&(index, next)) = chars.peek() {
412 if next.is_whitespace() || "(),!=<>".contains(next) {
413 break;
414 }
415 chars.next();
416 end = index + next.len_utf8();
417 }
418 tokens.push(&input[start..end]);
419 }
420 }
421 let mut pos = 0;
422 let mut equal = BTreeMap::new();
423 let mut expressions: Vec<(&str, String)> = Vec::new();
424 while pos < tokens.len() {
425 let negated = tokens[pos] == "!";
426 if negated {
427 pos += 1;
428 }
429 let key = *tokens.get(pos)?;
430 if !valid_key(key) {
431 return None;
432 }
433 pos += 1;
434 if negated && !matches!(tokens.get(pos), None | Some(&",")) {
435 return None;
436 }
437 match tokens.get(pos).copied() {
438 Some("=" | "==") => {
439 pos += 1;
440 let mut value = "";
441 if let Some(&next) = tokens.get(pos).filter(|value| **value != ",") {
442 if !valid_value(next) {
443 return None;
444 }
445 value = next;
446 pos += 1;
447 }
448 equal.insert(key, value);
449 }
450 Some(op @ ("in" | "notin")) => {
451 pos += 1;
452 if tokens.get(pos) != Some(&"(") {
453 return None;
454 }
455 pos += 1;
456 let mut expect_value = true;
457 let mut values = BTreeSet::new();
458 while let Some(&token) = tokens.get(pos) {
459 if token == ")" {
460 break;
461 }
462 if token == "," {
463 if expect_value {
464 values.insert("");
465 }
466 expect_value = true;
467 } else if expect_value && valid_value(token) {
468 values.insert(token);
469 expect_value = false;
470 } else {
471 return None;
472 }
473 pos += 1;
474 }
475 if tokens.get(pos) != Some(&")") {
476 return None;
477 }
478 if expect_value {
479 values.insert("");
480 }
481 expressions.push((
482 key,
483 format!(
484 "{key} {op} ({})",
485 values.into_iter().collect::<Vec<_>>().join(",")
486 ),
487 ));
488 pos += 1;
489 }
490 Some(",") | None => {
491 expressions.push((key, format!("{}{key}", if negated { "!" } else { "" })))
492 }
493 _ => return None,
494 }
495 if pos == tokens.len() {
496 break;
497 }
498 if tokens[pos] != "," || pos + 1 == tokens.len() {
499 return None;
500 }
501 pos += 1;
502 }
503 expressions.extend(
504 equal
505 .into_iter()
506 .map(|(key, value)| (key, format!("{key}={value}"))),
507 );
508 expressions.sort_by_key(|(key, _)| *key);
509 Some(
510 expressions
511 .into_iter()
512 .map(|(_, expression)| expression)
513 .collect::<Vec<_>>()
514 .join(","),
515 )
516}
517
518fn valid_key(key: &str) -> bool {
519 let (prefix, name) = key
520 .split_once('/')
521 .map_or((None, key), |(prefix, name)| (Some(prefix), name));
522 if let Some(prefix) = prefix
523 && (prefix.len() > 253
524 || prefix.is_empty()
525 || !prefix.split('.').all(|part| {
526 !part.is_empty()
527 && part
528 .bytes()
529 .next()
530 .is_some_and(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
531 && part
532 .bytes()
533 .last()
534 .is_some_and(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
535 && part
536 .bytes()
537 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'-')
538 }))
539 {
540 return false;
541 }
542 !name.is_empty() && valid_value(name)
543}
544
545fn valid_value(value: &str) -> bool {
546 value.len() <= 63
547 && (value.is_empty()
548 || (value
549 .bytes()
550 .next()
551 .is_some_and(|c| c.is_ascii_alphanumeric())
552 && value
553 .bytes()
554 .last()
555 .is_some_and(|c| c.is_ascii_alphanumeric())
556 && value
557 .bytes()
558 .all(|c| c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'.'))))
559}