pub mod completions;
pub mod interactive;
pub mod query;
pub mod records;
pub mod runner;
use clap::{Parser, Subcommand};
use clap_complete::Shell;
use std::path::PathBuf;
use crate::control_plane::policy::PolicyRule;
#[derive(Parser)]
#[command(name = "dns", about = "DNS Sync and Control with MCP", version)]
pub struct Cli {
#[arg(long, env = "DNSYNC_CONFIG")]
pub config: Option<PathBuf>,
#[arg(long = "server", env = "DNSYNC_SERVER")]
pub servers: Vec<String>,
#[arg(long)]
pub all: bool,
#[arg(long)]
pub base_url: Option<String>,
#[arg(long)]
pub token: Option<String>,
#[arg(long, env = "DNS_ACCESS", value_enum, value_delimiter = ',', num_args = 0..)]
pub access: Vec<PolicyRule>,
#[arg(long, env = "DNS_ALLOWED_ZONES", value_delimiter = ',')]
pub allow_zone: Vec<String>,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand)]
pub enum Command {
#[command(subcommand)]
Config(ConfigCmd),
Mcp,
#[command(subcommand)]
Zone(ZoneCmd),
#[command(subcommand)]
Record(RecordCmd),
#[command(alias = "q")]
Query(query::QueryArgs),
Sync {
profile: Option<String>,
#[arg(long)]
from: Option<String>,
#[arg(long)]
to: Option<String>,
#[arg(long = "zone", value_name = "ZONE")]
zone: Vec<String>,
#[arg(long = "map", value_name = "SRC=DST")]
map: Vec<String>,
#[arg(long)]
apply: bool,
#[arg(long)]
json: bool,
},
#[command(subcommand)]
Cache(CacheCmd),
Stats {
#[arg(long, default_value = "LastDay")]
r#type: String,
},
#[command(subcommand)]
Blocked(BlockedCmd),
#[command(subcommand)]
Allowed(AllowedCmd),
Settings {
#[arg(long)]
show_secrets: bool,
},
Logs {
#[arg(long, default_value_t = 50)]
lines: u32,
#[arg(long)]
start: Option<String>,
#[arg(long)]
end: Option<String>,
#[arg(long, value_enum)]
level: Option<crate::core::dns::logs::LogLevel>,
},
Completions { shell: Shell },
#[command(name = "_servers", hide = true)]
ServerIds,
}
#[derive(Subcommand)]
pub enum ConfigCmd {
Init {
#[arg(long)]
force: bool,
},
Print,
Add {
#[arg(long)]
id: Option<String>,
#[arg(long, default_value = "technitium")]
vendor: crate::control_plane::config::VendorKind,
#[arg(long)]
base_url: Option<String>,
#[arg(long)]
base_url_env: Option<String>,
#[arg(long)]
token_env: Option<String>,
#[arg(long)]
token: Option<String>,
#[arg(long)]
org_id: Option<String>,
#[arg(long)]
location: Option<crate::control_plane::config::ServerLocation>,
#[arg(long, value_enum, value_delimiter = ',', num_args = 0.., default_values = &["read", "write", "delete"])]
access: Vec<PolicyRule>,
#[arg(long, value_name = "ZONE")]
allow_zone: Vec<String>,
#[arg(long = "validation-endpoint", value_name = "NAME:TRANSPORT:ADDRESS")]
validation_endpoints: Vec<crate::control_plane::config::ValidationEndpointConfig>,
},
Server {
server_id: Option<String>,
#[command(subcommand)]
endpoint: Option<ServerEndpointCmd>,
},
}
#[derive(Subcommand)]
pub enum ServerEndpointCmd {
Dns {
#[arg(long)]
addr: Option<String>,
#[arg(long)]
timeout_ms: Option<u64>,
#[arg(long)]
disable: bool,
#[arg(long)]
clear: bool,
},
Dot {
#[arg(long)]
addr: Option<String>,
#[arg(long)]
server_name: Option<String>,
#[arg(long)]
timeout_ms: Option<u64>,
#[arg(long)]
disable: bool,
#[arg(long)]
clear: bool,
},
Doh {
#[arg(long)]
url: Option<String>,
#[arg(long)]
addr: Option<String>,
#[arg(long)]
server_name: Option<String>,
#[arg(long)]
timeout_ms: Option<u64>,
#[arg(long)]
disable: bool,
#[arg(long)]
clear: bool,
},
Doq {
#[arg(long)]
addr: Option<String>,
#[arg(long)]
server_name: Option<String>,
#[arg(long)]
timeout_ms: Option<u64>,
#[arg(long)]
disable: bool,
#[arg(long)]
clear: bool,
},
}
#[derive(Subcommand)]
pub enum ZoneCmd {
List {
#[arg(long, default_value_t = 1)]
page: u32,
#[arg(long, default_value_t = 50)]
per_page: u32,
},
Create {
zone: String,
#[arg(long, default_value = "Primary")]
r#type: String,
},
Delete { zone: String },
Enable { zone: String },
Disable { zone: String },
Import {
zone: String,
file: std::path::PathBuf,
#[command(flatten)]
options: crate::core::dns::zones::ZoneImportOptions,
},
Export {
zone: String,
#[arg(long, short)]
output: Option<std::path::PathBuf>,
},
Transfer {
zone: String,
#[arg(long)]
from: String,
#[arg(long)]
to: String,
#[arg(long, default_value_t = true)]
overwrite: bool,
#[arg(long, default_value_t = false)]
overwrite_zone: bool,
},
}
#[derive(Subcommand)]
pub enum RecordCmd {
List {
domain: Option<String>,
#[arg(long)]
zone: Option<String>,
#[arg(long)]
all_subdomains: bool,
#[arg(long = "server", value_name = "ID")]
servers: Vec<String>,
#[arg(long)]
use_local_ip: bool,
#[arg(long)]
json: bool,
},
Add {
#[arg(long)]
zone: String,
#[arg(long)]
domain: String,
#[arg(long, default_value_t = 3600)]
ttl: u32,
#[command(subcommand)]
record: crate::core::dns::records::RecordData,
},
Delete {
#[arg(long)]
zone: String,
#[arg(long)]
domain: String,
#[command(subcommand)]
record: crate::core::dns::records::RecordSelector,
},
}
#[derive(Subcommand)]
pub enum CacheCmd {
List {
#[arg(default_value = "")]
domain: String,
},
Delete { domain: String },
Flush,
}
#[derive(Subcommand)]
pub enum BlockedCmd {
List,
Add { domain: String },
Delete { domain: String },
}
#[derive(Subcommand)]
pub enum AllowedCmd {
List,
Add { domain: String },
Delete { domain: String },
}
#[cfg(test)]
mod tests {
use super::*;
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn technitium_env_vars_do_not_populate_global_overrides() {
let _guard = ENV_LOCK.lock().unwrap();
unsafe {
std::env::set_var("TECHNITIUM_BASE_URL", "http://technitium.local:5380");
std::env::set_var("TECHNITIUM_API_TOKEN", "technitium-token");
}
let cli = Cli::try_parse_from(["dns", "mcp"]).unwrap();
assert!(cli.base_url.is_none());
assert!(cli.token.is_none());
unsafe {
std::env::remove_var("TECHNITIUM_BASE_URL");
std::env::remove_var("TECHNITIUM_API_TOKEN");
}
}
#[test]
fn settings_accepts_show_secrets_flag() {
let cli = Cli::try_parse_from(["dns", "settings", "--show-secrets"]).unwrap();
assert!(matches!(
cli.command,
Command::Settings { show_secrets: true }
));
let cli = Cli::try_parse_from(["dns", "settings"]).unwrap();
assert!(matches!(
cli.command,
Command::Settings {
show_secrets: false
}
));
}
}