pharos-plugins 0.1.0

CoreDNS-compatible Plugins for the Pharos DNS server
Documentation
#![forbid(unsafe_code)]

use std::{env, fmt::Write, fs, path::PathBuf};

fn main() {
    println!("cargo:rerun-if-changed=plugin.cfg");
    let source = fs::read_to_string("plugin.cfg").expect("plugin.cfg");
    let enabled: Vec<_> = source
        .lines()
        .filter_map(|line| {
            let (name, _) = line.split_once(':')?;
            if name.starts_with('#') {
                return None;
            }
            let feature = format!("CARGO_FEATURE_PLUGIN_{}", name.to_ascii_uppercase());
            env::var_os(feature).map(|_| name)
        })
        .collect();

    let mut code = String::from(
        "#[derive(Debug, Default)]\npub struct Chain {\npub lifecycle: pharos_server::Lifecycle,\n",
    );
    for name in &enabled {
        let ty = match *name {
            "any" => "any::Any",
            "chaos" => "chaos::Chaos",
            "cache" => "cache::Cache",
            "bind" => "()",
            "bufsize" => "bufsize::Bufsize",
            "header" => "header::Header",
            "minimal" => "minimal::Minimal",
            "multisocket" => "Option<usize>",
            "whoami" => "whoami::Whoami",
            "log" => "log::Log",
            "metadata" => "metadata::Metadata",
            "loop" => "r#loop::Loop",
            "prometheus" => "prometheus::Prometheus",
            "ready" => "ready::Ready",
            "forward" => "forward::Forward",
            "health" => "health::Health",
            "hosts" => "hosts::Hosts",
            "kubernetes" => "kubernetes::Kubernetes",
            "loadbalance" => "loadbalance::LoadBalance",
            "local" => "local::Local",
            "nsid" => "nsid::Nsid",
            "reload" => "reload::Reload",
            "errors" => "errors::Errors",
            _ => "Unimplemented",
        };
        writeln!(code, "pub r#{name}: Option<{ty}>,").unwrap();
    }
    code.push_str("#[cfg(all(test, feature = \"plugin-cache\", feature = \"plugin-hosts\"))] pub self_lookup_test: Option<self_lookup_test::TestPlugin>,\n");
    code.push_str("}\npub static DIRECTIVES: &[pharos_corefile::Directive<'static, Chain>] = &[\n");
    for name in &enabled {
        let setup = match *name {
            "any" => "setup_any",
            "chaos" => "setup_chaos",
            "cache" => "setup_cache",
            "bind" => "setup_bind",
            "bufsize" => "setup_bufsize",
            "header" => "setup_header",
            "minimal" => "setup_minimal",
            "multisocket" => "setup_multisocket",
            "whoami" => "setup_whoami",
            "log" => "setup_log",
            "metadata" => "setup_metadata",
            "loop" => "setup_loop",
            "prometheus" => "setup_prometheus",
            "ready" => "setup_ready",
            "forward" => "setup_forward",
            "health" => "setup_health",
            "hosts" => "setup_hosts",
            "loadbalance" => "setup_loadbalance",
            "local" => "setup_local",
            "nsid" => "setup_nsid",
            "reload" => "setup_reload",
            "errors" => "setup_errors",
            "kubernetes" => "setup_kubernetes",
            _ => "setup_unimplemented",
        };
        let once = matches!(
            *name,
            "cache"
                | "metadata"
                | "prometheus"
                | "ready"
                | "errors"
                | "reload"
                | "hosts"
                | "loop"
                | "nsid"
                | "kubernetes"
        );
        writeln!(
            code,
            "pharos_corefile::Directive {{ name: {name:?}, once: {once}, setup: {setup} }},"
        )
        .unwrap();
    }
    code.push_str("];\n");
    let mut sorted = enabled.clone();
    sorted.sort_unstable();
    let listing = if sorted.is_empty() {
        "\n".to_owned()
    } else {
        sorted.join("\n") + "\n\n"
    };
    writeln!(code, "pub const PLUGINS_LISTING: &str = {listing:?};").unwrap();

    code.push_str("impl Chain {\n");
    if enabled.contains(&"ready") {
        code.push_str(
            "fn collect_readiness(self: &std::sync::Arc<Self>, state: &ready::State) {\nlet _ = state;\n",
        );
        for name in &enabled {
            if matches!(
                *name,
                "bind" | "multisocket" | "ready" | "health" | "reload"
            ) {
                continue;
            }
            writeln!(code, "if self.r#{name}.as_ref().and_then(pharos_server::Plugin::readiness).is_some() {{ let chain = std::sync::Arc::clone(self); state.append({name:?}, move || chain.r#{name}.as_ref().and_then(pharos_server::Plugin::readiness).is_some_and(pharos_server::Readiness::ready)); }}").unwrap();
        }
        code.push_str("}\n");
    }
    code.push_str("pub fn serve(&self, cx: &mut pharos_server::Context<'_>) -> impl std::future::Future<Output = pharos_server::Outcome> + Send { ");
    if enabled.contains(&"metadata") {
        code.push_str("cx.metadata.set_enabled(self.metadata.is_some()); ");
    } else {
        code.push_str("cx.metadata.set_enabled(false); ");
    }
    if enabled.contains(&"metadata") && enabled.contains(&"kubernetes") {
        code.push_str("if self.metadata.as_ref().is_some_and(|plugin| plugin.matches(cx)) && let Some(plugin) = &self.kubernetes { plugin.collect_metadata(cx); } ");
    }
    code.push_str("self.serve_0(cx) }\n");
    if let Some(index) = enabled.iter().position(|name| *name == "cache") {
        writeln!(code, "async fn cache_refresh_next(&self, cx: &mut pharos_server::Context<'_>) -> pharos_server::Outcome {{ #[cfg(all(test, feature = \"plugin-cache\", feature = \"plugin-hosts\"))] if let Some(plugin) = &self.self_lookup_test {{ return plugin.serve(cx).await; }} self.serve_{}(cx).await }}", index + 1).unwrap();
    }
    for (i, name) in enabled.iter().enumerate() {
        let next = i + 1;
        if matches!(
            *name,
            "bind" | "multisocket" | "ready" | "health" | "reload"
        ) {
            writeln!(code, "async fn serve_{i}(&self, cx: &mut pharos_server::Context<'_>) -> pharos_server::Outcome {{ self.serve_{next}(cx).await }}").unwrap();
            continue;
        }
        writeln!(code, "async fn serve_{i}(&self, cx: &mut pharos_server::Context<'_>) -> pharos_server::Outcome {{ match &self.r#{name} {{ Some(plugin) => {{ let outcome = pharos_server::Plugin::serve(plugin, cx, Next{i}(self)).await; if outcome == pharos_server::Outcome::Response && cx.metadata.get(pharos_server::metrics::WRITER_METADATA).is_none_or(str::is_empty) {{ cx.metadata.insert(pharos_server::metrics::WRITER_METADATA, {name:?}); }} else if matches!(outcome, pharos_server::Outcome::Error(_)) && cx.metadata.get(pharos_server::logging::PLUGIN_METADATA).is_none_or(str::is_empty) {{ cx.metadata.insert(pharos_server::logging::PLUGIN_METADATA, {name:?}); }} outcome }}, None => self.serve_{next}(cx).await, }} }}").unwrap();
    }
    writeln!(code, "async fn serve_{}(&self, _cx: &mut pharos_server::Context<'_>) -> pharos_server::Outcome {{ pharos_server::Outcome::Error(pharos_server::Error::NoNextPlugin) }}", enabled.len()).unwrap();
    code.push_str("}\n");
    code.push_str("impl pharos_server::Chain for Chain {\nfn serve(&self, cx: &mut pharos_server::Context<'_>) -> impl std::future::Future<Output = pharos_server::Outcome> + Send { Chain::serve(self, cx) }\nfn is_empty(&self) -> bool { true");
    for name in &enabled {
        if !matches!(
            *name,
            "bind" | "multisocket" | "ready" | "health" | "reload"
        ) {
            write!(code, " && self.r#{name}.is_none()").unwrap();
        }
    }
    code.push_str(
        " }\nfn visit_enabled(&self, visit: &mut dyn FnMut(&'static str)) {\nlet _ = &visit;\n",
    );
    for name in &enabled {
        if !matches!(
            *name,
            "bind" | "multisocket" | "ready" | "health" | "reload"
        ) {
            writeln!(code, "if self.r#{name}.is_some() {{ visit({name:?}); }}").unwrap();
        }
    }
    code.push_str("}\n");
    if enabled.contains(&"loop") {
        code.push_str("fn parallel_query(&self, query: &pharos_proto::Message<'_>) -> Option<tokio::sync::watch::Receiver<bool>> { self.r#loop.as_ref().and_then(|plugin| plugin.parallel_query(query)) }\n");
    }
    if enabled.contains(&"prometheus") {
        code.push_str("fn metrics_enabled(&self) -> bool { self.prometheus.is_some() }\n");
    } else {
        code.push_str("fn metrics_enabled(&self) -> bool { false }\n");
    }
    if enabled.contains(&"errors") {
        code.push_str("fn stacktrace(&self) -> bool { self.errors.as_ref().is_some_and(|plugin| plugin.stacktrace) }\n");
    }
    code.push_str("}\n");
    for (i, name) in enabled.iter().enumerate() {
        if matches!(
            *name,
            "bind" | "multisocket" | "ready" | "health" | "reload"
        ) {
            continue;
        }
        let next = i + 1;
        let test_hook = if *name == "cache" {
            "#[cfg(all(test, feature = \"plugin-cache\", feature = \"plugin-hosts\"))] if let Some(plugin) = &self.0.self_lookup_test { return plugin.serve(cx).await; }"
        } else {
            ""
        };
        writeln!(code, "struct Next{i}<'a>(&'a Chain);\nimpl pharos_server::Next for Next{i}<'_> {{ async fn run(self, cx: &mut pharos_server::Context<'_>) -> pharos_server::Outcome {{ {test_hook} self.0.serve_{next}(cx).await }} }}").unwrap();
    }
    fs::write(
        PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("plugins.rs"),
        code,
    )
    .unwrap();
}