Skip to main content

pharos_plugins/kubernetes/
setup.rs

1use 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                // shortcut: negative timeouts clamp to zero; preserve signed durations when wiring startup.
194                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                // ClientConfig rejects non-finite QPS even though CoreDNS accepts it here.
203                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    // Round directly to float32 to avoid double-rounding hexadecimal halfway values.
346    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}