use anyhow::{Context, Result};
use tracing::trace;
use crate::namespace::{ExportConfig, NamespaceConfig, ServiceConfig};
use crate::ListenerConfig;
use super::helpers::get_first_arg_string;
use super::{parse_filter_definitions, parse_listeners, parse_routes, parse_upstreams};
pub fn parse_namespace(node: &kdl::KdlNode) -> Result<NamespaceConfig> {
let id = get_first_arg_string(node)
.ok_or_else(|| anyhow::anyhow!("Namespace requires an ID as first argument"))?;
trace!(namespace_id = %id, "Parsing namespace");
let mut ns = NamespaceConfig::new(id.clone());
let children = node.children().ok_or_else(|| {
anyhow::anyhow!(
"Namespace '{}' requires a configuration block with braces {{}}",
id
)
})?;
for child in children.nodes() {
let child_name = child.name().value();
trace!(namespace = %id, block = child_name, "Parsing namespace child block");
match child_name {
"limits" => {
ns.limits = Some(super::parse_limits_config(child)?);
trace!(namespace = %id, "Parsed namespace limits");
}
"listeners" => {
ns.listeners = parse_listeners(child)?;
trace!(namespace = %id, count = ns.listeners.len(), "Parsed namespace listeners");
}
"upstreams" => {
ns.upstreams = parse_upstreams(child)?;
trace!(namespace = %id, count = ns.upstreams.len(), "Parsed namespace upstreams");
}
"routes" => {
ns.routes = parse_routes(child)?;
trace!(namespace = %id, count = ns.routes.len(), "Parsed namespace routes");
}
"agents" => {
ns.agents = super::parse_agents(child)?;
trace!(namespace = %id, count = ns.agents.len(), "Parsed namespace agents");
}
"filters" => {
ns.filters = parse_filter_definitions(child)?;
trace!(namespace = %id, count = ns.filters.len(), "Parsed namespace filters");
}
"service" => {
let service = parse_service(child)?;
trace!(namespace = %id, service = %service.id, "Parsed service");
ns.services.push(service);
}
"exports" => {
ns.exports = parse_exports(child)?;
trace!(namespace = %id, exports = ns.exports.len(), "Parsed exports");
}
other => {
return Err(anyhow::anyhow!(
"Unknown block '{}' in namespace '{}'\n\
Valid blocks are: limits, listeners, upstreams, routes, agents, filters, service, exports",
other, id
));
}
}
}
Ok(ns)
}
pub fn parse_service(node: &kdl::KdlNode) -> Result<ServiceConfig> {
let id = get_first_arg_string(node)
.ok_or_else(|| anyhow::anyhow!("Service requires an ID as first argument"))?;
trace!(service_id = %id, "Parsing service");
let mut svc = ServiceConfig::new(id.clone());
let children = node.children().ok_or_else(|| {
anyhow::anyhow!(
"Service '{}' requires a configuration block with braces {{}}",
id
)
})?;
for child in children.nodes() {
let child_name = child.name().value();
trace!(service = %id, block = child_name, "Parsing service child block");
match child_name {
"listener" => {
svc.listener = Some(parse_single_listener(child)?);
trace!(service = %id, "Parsed service listener");
}
"upstreams" => {
svc.upstreams = parse_upstreams(child)?;
trace!(service = %id, count = svc.upstreams.len(), "Parsed service upstreams");
}
"routes" => {
svc.routes = parse_routes(child)?;
trace!(service = %id, count = svc.routes.len(), "Parsed service routes");
}
"agents" => {
svc.agents = super::parse_agents(child)?;
trace!(service = %id, count = svc.agents.len(), "Parsed service agents");
}
"filters" => {
svc.filters = parse_filter_definitions(child)?;
trace!(service = %id, count = svc.filters.len(), "Parsed service filters");
}
"limits" => {
svc.limits = Some(super::parse_limits_config(child)?);
trace!(service = %id, "Parsed service limits");
}
other => {
return Err(anyhow::anyhow!(
"Unknown block '{}' in service '{}'\n\
Valid blocks are: listener, upstreams, routes, agents, filters, limits",
other,
id
));
}
}
}
Ok(svc)
}
fn parse_single_listener(node: &kdl::KdlNode) -> Result<ListenerConfig> {
use super::helpers::{get_int_entry, get_string_entry};
use crate::server::ListenerProtocol;
let id = get_first_arg_string(node)
.or_else(|| get_string_entry(node, "id"))
.unwrap_or_else(|| "service-listener".to_string());
let address = get_string_entry(node, "address")
.ok_or_else(|| anyhow::anyhow!("Listener '{}' requires an 'address'", id))?;
let protocol_str = get_string_entry(node, "protocol").unwrap_or_else(|| "http".to_string());
let protocol = match protocol_str.to_lowercase().as_str() {
"http" => ListenerProtocol::Http,
"https" => ListenerProtocol::Https,
"h2" | "http2" => ListenerProtocol::Http2,
"h3" | "http3" => ListenerProtocol::Http3,
other => {
return Err(anyhow::anyhow!(
"Invalid protocol '{}' for listener '{}'. Valid protocols: http, https, h2, h3",
other,
id
))
}
};
let tls = if let Some(children) = node.children() {
children
.nodes()
.iter()
.find(|n| n.name().value() == "tls")
.map(|n| super::server::parse_tls_config(n, &id))
.transpose()
.context("Failed to parse TLS config")?
} else {
None
};
Ok(ListenerConfig {
id,
address,
protocol,
tls,
default_route: get_string_entry(node, "default-route"),
request_timeout_secs: get_int_entry(node, "request-timeout-secs")
.map(|v| v as u64)
.unwrap_or(60),
keepalive_timeout_secs: get_int_entry(node, "keepalive-timeout-secs")
.map(|v| v as u64)
.unwrap_or(75),
max_concurrent_streams: get_int_entry(node, "max-concurrent-streams")
.map(|v| v as u32)
.unwrap_or(100),
keepalive_max_requests: get_int_entry(node, "keepalive-max-requests").map(|v| v as u32),
})
}
fn parse_exports(node: &kdl::KdlNode) -> Result<ExportConfig> {
let mut exports = ExportConfig::default();
let children = match node.children() {
Some(c) => c,
None => return Ok(exports), };
for child in children.nodes() {
let child_name = child.name().value();
match child_name {
"upstreams" => {
exports.upstreams = parse_string_list(child);
}
"agents" => {
exports.agents = parse_string_list(child);
}
"filters" => {
exports.filters = parse_string_list(child);
}
other => {
return Err(anyhow::anyhow!(
"Unknown export type '{}'\n\
Valid export types are: upstreams, agents, filters",
other
));
}
}
}
Ok(exports)
}
fn parse_string_list(node: &kdl::KdlNode) -> Vec<String> {
node.entries()
.iter()
.filter_map(|entry| {
if entry.name().is_none() {
entry.value().as_string().map(|s| s.to_string())
} else {
None
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_kdl_node(kdl: &str) -> kdl::KdlNode {
let doc: kdl::KdlDocument = kdl.parse().unwrap();
doc.nodes().first().unwrap().clone()
}
#[test]
fn test_parse_empty_namespace() {
let kdl = r#"namespace "api" {}"#;
let node = parse_kdl_node(kdl);
let ns = parse_namespace(&node).unwrap();
assert_eq!(ns.id, "api");
assert!(ns.is_empty());
}
#[test]
fn test_parse_namespace_with_upstreams() {
let kdl = r#"
namespace "api" {
upstreams {
upstream "backend" {
target "127.0.0.1:8080"
}
}
}
"#;
let node = parse_kdl_node(kdl);
let ns = parse_namespace(&node).unwrap();
assert_eq!(ns.id, "api");
assert_eq!(ns.upstreams.len(), 1);
assert!(ns.upstreams.contains_key("backend"));
}
#[test]
fn test_parse_empty_service() {
let kdl = r#"service "payments" {}"#;
let node = parse_kdl_node(kdl);
let svc = parse_service(&node).unwrap();
assert_eq!(svc.id, "payments");
assert!(svc.is_empty());
}
#[test]
fn test_parse_service_with_listener() {
let kdl = r#"
service "payments" {
listener {
id "payments-https"
address "0.0.0.0:8443"
protocol "https"
}
}
"#;
let node = parse_kdl_node(kdl);
let svc = parse_service(&node).unwrap();
assert_eq!(svc.id, "payments");
assert!(svc.listener.is_some());
let listener = svc.listener.unwrap();
assert_eq!(listener.id, "payments-https");
assert_eq!(listener.address, "0.0.0.0:8443");
}
#[test]
fn test_parse_exports() {
let kdl = r#"
exports {
upstreams "shared-auth" "shared-cache"
agents "global-waf"
}
"#;
let node = parse_kdl_node(kdl);
let exports = parse_exports(&node).unwrap();
assert_eq!(exports.upstreams.len(), 2);
assert_eq!(exports.upstreams[0], "shared-auth");
assert_eq!(exports.upstreams[1], "shared-cache");
assert_eq!(exports.agents.len(), 1);
assert_eq!(exports.agents[0], "global-waf");
}
#[test]
fn test_parse_string_list() {
let kdl = r#"items "a" "b" "c""#;
let node = parse_kdl_node(kdl);
let list = parse_string_list(&node);
assert_eq!(list, vec!["a", "b", "c"]);
}
#[test]
fn test_namespace_requires_id() {
let kdl = r#"namespace {}"#;
let node = parse_kdl_node(kdl);
let result = parse_namespace(&node);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("requires an ID"));
}
#[test]
fn test_service_requires_id() {
let kdl = r#"service {}"#;
let node = parse_kdl_node(kdl);
let result = parse_service(&node);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("requires an ID"));
}
}