pharos-plugins 0.1.0

CoreDNS-compatible Plugins for the Pharos DNS server
Documentation
#![forbid(unsafe_code)]
#![cfg(feature = "plugin-kubernetes")]
#![recursion_limit = "512"]

use pharos_corefile::{Cursor, configure, parse};
use pharos_plugins::{
    Chain, directives,
    kubernetes::setup::{Config, parse as parse_setup},
};

fn config(text: &str) -> Result<Config, pharos_corefile::Error> {
    let blocks = parse("Corefile", &format!(".:53 {{\n{text}\n}}"))?;
    let tokens = &blocks[0].directives["kubernetes"];
    parse_setup(&mut Cursor::new("Corefile", tokens), &["."])
}

#[test]
fn defaults_and_ttl_match_coredns() {
    let k = config("kubernetes cluster.local").unwrap();
    assert_eq!(k.zones, ["cluster.local."]);
    assert_eq!(k.ttl, 5);
    assert_eq!(k.pod_mode, "disabled");
    assert!(k.endpoints);
    assert_eq!(k.startup_timeout, std::time::Duration::from_secs(5));
    for (ttl, expected) in [
        ("0", Some(0)),
        ("-0", Some(0)),
        ("56", Some(56)),
        ("3600", Some(3600)),
        ("-1", None),
        ("3601", None),
    ] {
        let result = config(&format!("kubernetes cluster.local {{\n ttl {ttl}\n}}"));
        assert_eq!(result.ok().map(|k| k.ttl), expected, "{ttl}");
    }
}

#[test]
fn setup_registers_backend_and_validates_options() {
    let blocks = parse("Corefile", ".:53 {\n kubernetes cluster.local\n}").unwrap();
    assert!(
        configure::<Chain>(&blocks, directives()).unwrap()[0]
            .config
            .kubernetes
            .is_some()
    );
    let blocks = parse(
        "Corefile",
        ".:53 {\n kubernetes cluster.local {\n  pods invalid\n }\n}",
    )
    .unwrap();
    assert!(
        configure::<Chain>(&blocks, directives())
            .unwrap_err()
            .message
            .contains("wrong value for pods")
    );
    let blocks = parse(
        "Corefile",
        ".:53 {\n kubernetes first.local\n kubernetes second.local\n}",
    )
    .unwrap();
    assert!(
        configure::<Chain>(&blocks, directives())
            .unwrap_err()
            .message
            .contains("only be used once")
    );
}

#[test]
fn coredns_setup_variants_and_defaults() {
    assert_eq!(config("kubernetes").unwrap().zones, ["."]);
    let k = config("kubernetes 10.in-addr.arpa cluster.local {\n pods insecure\n kubeconfig file\n ignore\n ttl 1 extra\n startup_timeout 1s extra\n}").unwrap();
    assert_eq!(k.primary_zone_index, 1);
    assert_eq!(k.pod_mode, "insecure");
    assert_eq!(k.kubeconfig, Some(("file".into(), None)));
    assert!(!k.ignore_empty_service);
    assert_eq!(k.ttl, 1);
    assert!(config("kubernetes cluster.local {\n pods disabled\n}").is_ok());
    assert!(config("kubernetes cluster.local {\n labels\n}").is_err());
    assert!(config("kubernetes cluster.local {\n namespace_labels\n}").is_err());
    assert!(config("kubernetes first.local\n kubernetes second.local").is_err());
}

#[test]
fn parses_coredns_options() {
    let k = config("kubernetes cluster.local in-addr.arpa ip6.arpa {\n endpoint https://one https://two\n tls cert key ca\n kubeconfig file ctx\n namespaces demo test demo\n labels environment in (production, staging, qa),application=nginx\n pods verified\n endpoint_pod_names\n fallthrough in-addr.arpa ip6.arpa\n ttl 56\n ignore empty_service extra\n startup_timeout 1s\n apiserver_qps 50\n apiserver_burst 100\n apiserver_max_inflight 25\n zonal\n}").unwrap();
    assert_eq!(k.primary_zone_index, 0);
    assert_eq!(k.namespaces.len(), 2);
    assert_eq!(k.endpoints_list, ["https://one", "https://two"]);
    assert_eq!(k.tls, Some(["cert".into(), "key".into(), "ca".into()]));
    assert_eq!(k.kubeconfig, Some(("file".into(), Some("ctx".into()))));
    assert_eq!(
        k.label_selector.as_deref(),
        Some("application=nginx,environment in (production,qa,staging)")
    );
    assert_eq!(k.pod_mode, "verified");
    assert!(k.endpoint_pod_names && k.ignore_empty_service && k.zonal);
    assert_eq!(k.fallthrough.domains, ["in-addr.arpa.", "ip6.arpa."]);
    assert_eq!(k.startup_timeout, std::time::Duration::from_secs(1));
    assert_eq!(
        (k.api_qps, k.api_burst, k.api_max_inflight),
        (50.0, 100, 25)
    );
    let client = k.client_config();
    assert_eq!(client.endpoint.as_deref(), Some("https://one"));
    assert_eq!(client.kubeconfig, Some(("file".into(), Some("ctx".into()))));
    let tls = client.tls.unwrap();
    assert_eq!(
        (tls.cert, tls.key, tls.ca),
        ("cert".into(), "key".into(), "ca".into())
    );
    assert_eq!(
        (client.api_qps, client.api_burst, client.api_max_inflight),
        (50.0, 100, 25)
    );
    let watch = k.watch_config();
    assert_eq!(watch.namespaces.len(), 2);
    assert_eq!(watch.label_selector, k.label_selector);
    assert!(watch.pods_verified && watch.endpoints && watch.zonal);
}

#[test]
fn validates_coredns_options() {
    for option in [
        "endpoint",
        "namespaces",
        "tls a b",
        "pods",
        "pods bad",
        "pods insecure extra",
        "endpoint_pod_names extra",
        "noendpoints extra",
        "zonal extra",
        "kubeconfig",
        "kubeconfig a b c",
        "startup_timeout",
        "startup_timeout nonsense",
        "apiserver_qps -1",
        "apiserver_qps nope",
        "apiserver_burst -1",
        "apiserver_max_inflight -1",
        "apiserver_burst 9223372036854775808",
        "ignore bad",
        "unknown",
    ] {
        assert!(
            config(&format!("kubernetes cluster.local {{\n {option}\n}}")).is_err(),
            "{option}"
        );
    }
    for text in [
        "kubernetes 10.in-addr.arpa a.ip6.arpa",
        "kubernetes cluster.local {\n namespaces a\n namespace_labels app=demo\n}",
        "kubernetes cluster.local {\n noendpoints\n zonal\n}",
    ] {
        assert!(config(text).is_err(), "{text}");
    }
    let k = config(
        "kubernetes cluster.local {\n noendpoints\n fallthrough\n namespace_labels app=demo\n}",
    )
    .unwrap();
    assert!(!k.endpoints);
    assert!(!k.watch_config().endpoints);
    assert_eq!(
        k.watch_config().namespace_label_selector,
        k.namespace_label_selector
    );
    assert_eq!(k.namespace_label_selector.as_deref(), Some("app=demo"));
    assert_eq!(k.fallthrough.domains, ["."]);
}

#[test]
fn validates_label_selector_grammar() {
    for selector in [
        "app=nginx",
        "example.com/app in (foo, bar),!disabled",
        "a=,b==value,c notin (x,y)",
        "foo",
        "foo in ()",
        "app in (a,,b)",
    ] {
        assert!(
            config(&format!(
                "kubernetes cluster.local {{\n labels {selector}\n}}"
            ))
            .is_ok(),
            "{selector}"
        );
    }
    for selector in [
        "environment in (production, qa",
        "bad key=value",
        "app > 1",
        "a!=value",
        "!a=value",
        "!a in (b)",
        "UPPER.example/app=x",
        "bad/=x",
        "app=x,",
    ] {
        assert!(
            config(&format!(
                "kubernetes cluster.local {{\n labels {selector}\n}}"
            ))
            .is_err(),
            "{selector}"
        );
    }
    let k = config("kubernetes cluster.local {\n labels z=first,a in (z,a,z),z=last\n}").unwrap();
    assert_eq!(k.label_selector.as_deref(), Some("a in (a,z),z=last"));
    let long_prefix = format!("{}/app=ok", "a".repeat(64));
    assert!(
        config(&format!(
            "kubernetes cluster.local {{\n labels {long_prefix}\n}}"
        ))
        .is_ok()
    );
}

#[test]
fn multicluster_setup_matches_coredns() {
    let invalid =
        config("kubernetes cluster.local {\n multicluster clusterset.local\n}").unwrap_err();
    assert!(invalid.message.contains("not authoritative"), "{invalid}");
    let invalid_extra = config(
        "kubernetes cluster.local clusterset.local {\n multicluster clusterset.local test.local\n}",
    )
    .unwrap_err();
    assert!(
        invalid_extra.message.contains("not authoritative"),
        "{invalid_extra}"
    );
    let configured =
        config("kubernetes cluster.local clusterset.local {\n multicluster clusterset.local\n}")
            .unwrap();
    assert_eq!(configured.multicluster_zones, ["clusterset.local."]);
    assert!(configured.watch_config().multicluster);
    let empty = config("kubernetes cluster.local {\n multicluster\n}").unwrap();
    assert!(empty.multicluster_zones.is_empty());
    assert!(!empty.watch_config().multicluster);
    for zones in ["bad..zone", "bad..zone cluster.local"] {
        let parsed = config(&format!(
            "kubernetes cluster.local {{\n multicluster {zones}\n}}"
        ))
        .unwrap();
        assert!(
            !parsed.watch_config().multicluster || parsed.multicluster_zones == ["cluster.local."]
        );
    }
    let invalid =
        config("kubernetes cluster.local {\n multicluster cluster.local\n zonal\n noendpoints\n}")
            .unwrap_err();
    assert!(invalid.message.contains("zonal requires"), "{invalid}");
}

#[test]
fn qps_follows_go_float32_syntax_range_and_rounding() {
    for (input, expected) in [
        ("0x1p4", 16.0f32.to_bits()),
        ("0X1.8P2", 6.0f32.to_bits()),
        ("0x_1p2", 4.0f32.to_bits()),
        ("1_2.5", 12.5f32.to_bits()),
        ("0x1p1_0", 1024.0f32.to_bits()),
        ("0x1.000001p0", 1.0f32.to_bits()),
        ("0x1.000003p0", 0x3f800002),
        ("0x1p-150", 0),
        ("0x1.000001p-150", 1),
        ("0x1p-149", 1),
        ("0x1.fffffep127", f32::MAX.to_bits()),
        ("0x0p999999999", 0),
        ("3.4028235e38", f32::MAX.to_bits()),
        ("1e-9999", 0),
        ("-1e-9999", (-0.0f32).to_bits()),
    ] {
        let parsed = config(&format!(
            "kubernetes cluster.local {{\n apiserver_qps {input}\n}}"
        ))
        .unwrap_or_else(|error| panic!("{input}: {error}"));
        assert_eq!(parsed.api_qps.to_bits(), expected, "{input}");
    }
    for input in ["NaN", "nan", "NAN"] {
        assert!(
            config(&format!(
                "kubernetes cluster.local {{\n apiserver_qps {input}\n}}"
            ))
            .unwrap()
            .api_qps
            .is_nan(),
            "{input}"
        );
    }
    for input in ["+Inf", "Infinity", "infinity"] {
        assert_eq!(
            config(&format!(
                "kubernetes cluster.local {{\n apiserver_qps {input}\n}}"
            ))
            .unwrap()
            .api_qps,
            f32::INFINITY,
            "{input}"
        );
    }
    for input in [
        "--1",
        "++1",
        "--0",
        "+-0",
        "-+0",
        "1e39",
        "-1e39",
        "3.4028236e38",
        "0x1.ffffffp127",
        "0x1p999999999",
        "-Inf",
        "+NaN",
        "1__2",
        "1e_3",
        "0x1",
        "0x.p0",
        "0x1_p2",
        "0x1p_2",
    ] {
        assert!(
            config(&format!(
                "kubernetes cluster.local {{\n apiserver_qps {input}\n}}"
            ))
            .is_err(),
            "{input}"
        );
    }
}

#[test]
fn startup_timeout_accepts_zero_and_clamps_negative_values() {
    for input in ["0", "0s", "-0", "-0s", "-5s", "-1h30m"] {
        let result = config(&format!(
            "kubernetes cluster.local {{\n startup_timeout {input}\n}}"
        ))
        .unwrap_or_else(|error| panic!("{input}: {error}"));
        assert_eq!(result.startup_timeout, std::time::Duration::ZERO, "{input}");
    }
}