use clap::{ArgAction, Args, Parser, Subcommand};
use std::path::PathBuf;
pub const DEFAULT_HTTP_PORT: u16 = 5641;
#[cfg(windows)]
const WINDOWS_PIPE_NAME: &str = r"\\.\pipe\koi";
#[cfg(unix)]
const UNIX_SOCKET_FILENAME: &str = "koi.sock";
#[cfg(unix)]
const UNIX_FALLBACK_RUNTIME_DIR: &str = "/var/run";
#[derive(Parser, Debug)]
#[command(name = "koi", version, about = "Local service discovery for everyone")]
pub struct Cli {
#[arg(long)]
pub daemon: bool,
#[arg(long, env = "KOI_PORT", default_value = "5641")]
pub port: u16,
#[arg(long, env = "KOI_HTTP_BIND", default_value = "loopback")]
pub http_bind: String,
#[arg(long, env = "KOI_MTLS_PORT", default_value = "5642")]
pub mtls_port: u16,
#[arg(long, env = "KOI_PIPE")]
pub pipe: Option<PathBuf>,
#[arg(long, env = "KOI_LOG", default_value = "info")]
pub log_level: String,
#[arg(short, long, action = ArgAction::Count, global = true)]
pub verbose: u8,
#[arg(long, env = "KOI_LOG_FILE", value_name = "PATH", global = true)]
pub log_file: Option<PathBuf>,
#[arg(long, env = "KOI_NO_HTTP")]
pub no_http: bool,
#[arg(long, env = "KOI_NO_IPC")]
pub no_ipc: bool,
#[arg(long, env = "KOI_NO_MDNS")]
pub no_mdns: bool,
#[arg(long, env = "KOI_NO_CERTMESH")]
pub no_certmesh: bool,
#[arg(long, env = "KOI_NO_DNS")]
pub no_dns: bool,
#[arg(long, env = "KOI_NO_HEALTH")]
pub no_health: bool,
#[arg(long, env = "KOI_NO_PROXY")]
pub no_proxy: bool,
#[arg(long, env = "KOI_NO_UDP")]
pub no_udp: bool,
#[arg(long, env = "KOI_NO_RUNTIME")]
pub no_runtime: bool,
#[arg(long, env = "KOI_RUNTIME", default_value = "auto")]
pub runtime: String,
#[arg(long, env = "KOI_DNS_PORT", default_value = "53")]
pub dns_port: u16,
#[arg(long, env = "KOI_DNS_ZONE", default_value = "lan")]
pub dns_zone: String,
#[arg(long, env = "KOI_DNS_PUBLIC")]
pub dns_public: bool,
#[arg(long, env = "KOI_ANNOUNCE_HTTP")]
pub announce_http: bool,
#[arg(long, global = true)]
pub json: bool,
#[arg(long, global = true, value_name = "SECONDS")]
pub timeout: Option<u64>,
#[arg(long, env = "KOI_ENDPOINT", global = true)]
pub endpoint: Option<String>,
#[arg(long, global = true)]
pub standalone: bool,
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Subcommand, Debug)]
pub enum Command {
Install,
Uninstall,
Version,
Launch,
Status,
Mdns(MdnsCommand),
Certmesh(CertmeshCommand),
Dns(DnsCommand),
Health(HealthCommand),
Proxy(ProxyCommand),
Udp(UdpCommand),
Token(TokenCommand),
#[command(name = "factory-reset")]
FactoryReset,
}
#[derive(Args, Debug)]
pub struct TokenCommand {
#[command(subcommand)]
pub command: Option<TokenSubcommand>,
}
#[derive(Subcommand, Debug)]
pub enum TokenSubcommand {
Show {
#[arg(long)]
force: bool,
},
Write {
path: PathBuf,
},
}
#[derive(Args, Debug)]
pub struct MdnsCommand {
#[command(subcommand)]
pub command: Option<MdnsSubcommand>,
}
#[derive(Subcommand, Debug)]
pub enum MdnsSubcommand {
Discover {
service_type: Option<String>,
},
Announce {
name: String,
service_type: String,
port: u16,
#[arg(long)]
ip: Option<String>,
#[arg(trailing_var_arg = true)]
txt: Vec<String>,
},
Unregister {
id: String,
},
Resolve {
instance: String,
},
Subscribe {
service_type: String,
},
Admin(MdnsAdminCommand),
}
#[derive(Args, Debug)]
pub struct MdnsAdminCommand {
#[command(subcommand)]
pub command: Option<AdminSubcommand>,
}
#[derive(Subcommand, Debug)]
pub enum AdminSubcommand {
Status,
#[command(name = "ls")]
List,
Inspect {
id: String,
},
Unregister {
id: String,
},
Drain {
id: String,
},
Revive {
id: String,
},
}
#[derive(Args, Debug)]
pub struct CertmeshCommand {
#[command(subcommand)]
pub command: Option<CertmeshSubcommand>,
}
#[derive(Args, Debug)]
pub struct DnsCommand {
#[command(subcommand)]
pub command: Option<DnsSubcommand>,
}
#[derive(Args, Debug)]
pub struct HealthCommand {
#[command(subcommand)]
pub command: Option<HealthSubcommand>,
}
#[derive(Args, Debug)]
pub struct ProxyCommand {
#[command(subcommand)]
pub command: Option<ProxySubcommand>,
}
#[derive(Args, Debug)]
pub struct UdpCommand {
#[command(subcommand)]
pub command: Option<UdpSubcommand>,
}
#[derive(Subcommand, Debug)]
pub enum UdpSubcommand {
Bind {
#[arg(long, default_value = "0")]
port: u16,
#[arg(long, default_value = "0.0.0.0")]
addr: String,
#[arg(long, default_value = "300")]
lease: u64,
},
Unbind {
id: String,
},
Send {
id: String,
#[arg(long)]
dest: String,
payload: String,
},
Status,
Heartbeat {
id: String,
},
}
#[derive(Subcommand, Debug)]
pub enum DnsSubcommand {
Serve,
Stop,
Status,
Lookup {
name: String,
#[arg(long, default_value = "A")]
record_type: String,
},
Add {
name: String,
ip: String,
#[arg(long)]
ttl: Option<u32>,
},
Remove {
name: String,
},
List,
}
#[derive(Subcommand, Debug)]
pub enum HealthSubcommand {
Status,
Watch {
#[arg(long, default_value = "2")]
interval: u64,
},
Add {
name: String,
#[arg(long)]
http: Option<String>,
#[arg(long)]
tcp: Option<String>,
#[arg(long, default_value = "30")]
interval: u64,
#[arg(long, default_value = "5")]
timeout: u64,
},
Remove {
name: String,
},
Log,
}
#[derive(Subcommand, Debug)]
pub enum ProxySubcommand {
Add {
name: String,
#[arg(long)]
listen: u16,
#[arg(long)]
backend: String,
#[arg(long)]
backend_remote: bool,
},
Remove {
name: String,
},
Status,
List,
}
#[derive(Subcommand, Debug)]
pub enum CertmeshSubcommand {
Create {
#[arg(long)]
profile: Option<String>,
#[arg(long)]
operator: Option<String>,
#[arg(long)]
enrollment: Option<String>,
#[arg(long)]
require_approval: Option<bool>,
#[arg(long)]
passphrase: Option<String>,
},
Join {
endpoint: Option<String>,
},
Status,
Log,
Compliance,
Unlock,
SetHook {
#[arg(long)]
reload: String,
},
Promote {
endpoint: Option<String>,
},
OpenEnrollment {
#[arg(long)]
until: Option<String>,
},
CloseEnrollment,
SetPolicy {
#[arg(long)]
domain: Option<String>,
#[arg(long)]
subnet: Option<String>,
#[arg(long)]
clear: bool,
},
RotateAuth,
Backup {
path: PathBuf,
},
Restore {
path: PathBuf,
},
Revoke {
hostname: String,
#[arg(long)]
reason: Option<String>,
},
Destroy,
}
pub struct Config {
pub http_port: u16,
pub mtls_port: u16,
pub pipe_path: PathBuf,
pub no_http: bool,
pub no_ipc: bool,
pub no_mdns: bool,
pub no_certmesh: bool,
pub no_dns: bool,
pub no_health: bool,
pub no_proxy: bool,
pub no_udp: bool,
pub no_runtime: bool,
pub runtime: String,
pub announce_http: bool,
pub dns_port: u16,
pub dns_zone: String,
pub dns_public: bool,
pub http_bind: String,
}
impl Config {
pub fn from_cli(cli: &Cli) -> Self {
let pipe_path = cli.pipe.clone().unwrap_or_else(default_pipe_path);
Self {
http_port: cli.port,
mtls_port: cli.mtls_port,
pipe_path,
http_bind: cli.http_bind.clone(),
no_http: cli.no_http,
no_ipc: cli.no_ipc,
no_mdns: cli.no_mdns,
no_certmesh: cli.no_certmesh,
no_dns: cli.no_dns,
no_health: cli.no_health,
no_proxy: cli.no_proxy,
no_udp: cli.no_udp,
no_runtime: cli.no_runtime,
runtime: cli.runtime.clone(),
announce_http: cli.announce_http,
dns_port: cli.dns_port,
dns_zone: cli.dns_zone.clone(),
dns_public: cli.dns_public,
}
}
pub fn require_capability(&self, name: &str) -> anyhow::Result<()> {
let disabled = match name {
"mdns" => self.no_mdns,
"certmesh" => self.no_certmesh,
"dns" => self.no_dns,
"health" => self.no_health,
"proxy" => self.no_proxy,
"udp" => self.no_udp,
"runtime" => self.no_runtime,
_ => false,
};
if disabled {
anyhow::bail!(
"The '{name}' capability is disabled. \
Remove --no-{name} or unset KOI_NO_{} to enable it.",
name.to_uppercase().replace('-', "_")
);
}
Ok(())
}
pub fn dns_config(&self) -> koi_dns::DnsConfig {
koi_dns::DnsConfig {
port: self.dns_port,
zone: self.dns_zone.clone(),
allow_public_clients: self.dns_public,
..Default::default()
}
}
#[cfg(windows)]
pub fn from_env() -> Self {
let http_port = std::env::var("KOI_PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(DEFAULT_HTTP_PORT);
let mtls_port = std::env::var("KOI_MTLS_PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(crate::adapters::mtls::DEFAULT_MTLS_PORT);
let pipe_path = std::env::var("KOI_PIPE")
.ok()
.map(PathBuf::from)
.unwrap_or_else(default_pipe_path);
let no_http = std::env::var("KOI_NO_HTTP")
.ok()
.map(|s| s == "1" || s.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let no_ipc = std::env::var("KOI_NO_IPC")
.ok()
.map(|s| s == "1" || s.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let no_mdns = std::env::var("KOI_NO_MDNS")
.ok()
.map(|s| s == "1" || s.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let no_certmesh = std::env::var("KOI_NO_CERTMESH")
.ok()
.map(|s| s == "1" || s.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let no_dns = std::env::var("KOI_NO_DNS")
.ok()
.map(|s| s == "1" || s.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let no_health = std::env::var("KOI_NO_HEALTH")
.ok()
.map(|s| s == "1" || s.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let no_proxy = std::env::var("KOI_NO_PROXY")
.ok()
.map(|s| s == "1" || s.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let no_udp = std::env::var("KOI_NO_UDP")
.ok()
.map(|s| s == "1" || s.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let no_runtime = std::env::var("KOI_NO_RUNTIME")
.ok()
.map(|s| s == "1" || s.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let runtime = std::env::var("KOI_RUNTIME").unwrap_or_else(|_| "auto".to_string());
let announce_http = std::env::var("KOI_ANNOUNCE_HTTP")
.ok()
.map(|s| s == "1" || s.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let dns_port = std::env::var("KOI_DNS_PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(53);
let dns_zone = std::env::var("KOI_DNS_ZONE").unwrap_or_else(|_| "lan".to_string());
let dns_public = std::env::var("KOI_DNS_PUBLIC")
.ok()
.map(|s| s == "1" || s.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let http_bind = std::env::var("KOI_HTTP_BIND").unwrap_or_else(|_| "loopback".to_string());
Self {
http_port,
mtls_port,
pipe_path,
http_bind,
no_http,
no_ipc,
no_mdns,
no_certmesh,
no_dns,
no_health,
no_proxy,
no_udp,
no_runtime,
runtime,
announce_http,
dns_port,
dns_zone,
dns_public,
}
}
}
impl Default for Config {
fn default() -> Self {
Self {
http_port: DEFAULT_HTTP_PORT,
mtls_port: crate::adapters::mtls::DEFAULT_MTLS_PORT,
pipe_path: default_pipe_path(),
http_bind: "loopback".to_string(),
no_http: false,
no_ipc: false,
no_mdns: false,
no_certmesh: false,
no_dns: false,
no_health: false,
no_proxy: false,
no_udp: false,
no_runtime: false,
runtime: "auto".to_string(),
announce_http: false,
dns_port: 53,
dns_zone: "lan".to_string(),
dns_public: false,
}
}
}
fn default_pipe_path() -> PathBuf {
#[cfg(windows)]
{
PathBuf::from(WINDOWS_PIPE_NAME)
}
#[cfg(unix)]
{
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
PathBuf::from(runtime_dir).join(UNIX_SOCKET_FILENAME)
} else {
PathBuf::from(UNIX_FALLBACK_RUNTIME_DIR).join(UNIX_SOCKET_FILENAME)
}
}
#[cfg(not(any(unix, windows)))]
{
PathBuf::from(UNIX_SOCKET_FILENAME)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn require_capability_passes_when_enabled() {
let config = Config::default();
assert!(config.require_capability("mdns").is_ok());
assert!(config.require_capability("certmesh").is_ok());
assert!(config.require_capability("dns").is_ok());
assert!(config.require_capability("health").is_ok());
assert!(config.require_capability("proxy").is_ok());
}
#[test]
fn require_capability_fails_when_mdns_disabled() {
let config = Config {
no_mdns: true,
..Config::default()
};
let result = config.require_capability("mdns");
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("mdns"),
"error message should mention 'mdns': {msg}"
);
assert!(
msg.contains("disabled"),
"error message should mention 'disabled': {msg}"
);
}
#[test]
fn require_capability_fails_when_certmesh_disabled() {
let config = Config {
no_certmesh: true,
..Config::default()
};
let result = config.require_capability("certmesh");
assert!(result.is_err());
}
#[test]
fn require_capability_fails_when_dns_disabled() {
let config = Config {
no_dns: true,
..Config::default()
};
let result = config.require_capability("dns");
assert!(result.is_err());
}
#[test]
fn require_capability_fails_when_health_disabled() {
let config = Config {
no_health: true,
..Config::default()
};
let result = config.require_capability("health");
assert!(result.is_err());
}
#[test]
fn require_capability_fails_when_proxy_disabled() {
let config = Config {
no_proxy: true,
..Config::default()
};
let result = config.require_capability("proxy");
assert!(result.is_err());
}
#[test]
fn require_capability_unknown_name_passes() {
let config = Config::default();
assert!(config.require_capability("unknown").is_ok());
}
#[test]
fn config_default_values() {
let config = Config::default();
assert_eq!(config.http_port, DEFAULT_HTTP_PORT);
assert!(!config.no_http);
assert!(!config.no_ipc);
assert!(!config.no_mdns);
assert!(!config.no_certmesh);
assert!(!config.no_dns);
assert!(!config.no_health);
assert!(!config.no_proxy);
assert_eq!(config.dns_port, 53);
assert_eq!(config.dns_zone, "lan");
assert!(!config.dns_public);
}
#[test]
fn default_http_port_is_5641() {
assert_eq!(DEFAULT_HTTP_PORT, 5641);
}
#[test]
fn parse_certmesh_promote_no_endpoint() {
let cli = Cli::try_parse_from(["koi", "certmesh", "promote"]).unwrap();
match cli.command {
Some(Command::Certmesh(CertmeshCommand {
command: Some(CertmeshSubcommand::Promote { endpoint }),
})) => {
assert!(endpoint.is_none());
}
other => panic!("Expected Certmesh Promote, got: {other:?}"),
}
}
#[test]
fn parse_certmesh_promote_with_endpoint() {
let cli = Cli::try_parse_from(["koi", "certmesh", "promote", "http://ca:5641"]).unwrap();
match cli.command {
Some(Command::Certmesh(CertmeshCommand {
command: Some(CertmeshSubcommand::Promote { endpoint }),
})) => {
assert_eq!(endpoint.as_deref(), Some("http://ca:5641"));
}
other => panic!("Expected Certmesh Promote, got: {other:?}"),
}
}
#[test]
fn parse_certmesh_set_hook() {
let cli = Cli::try_parse_from([
"koi",
"certmesh",
"set-hook",
"--reload",
"systemctl restart nginx",
])
.unwrap();
match cli.command {
Some(Command::Certmesh(CertmeshCommand {
command: Some(CertmeshSubcommand::SetHook { reload }),
})) => {
assert_eq!(reload, "systemctl restart nginx");
}
other => panic!("Expected Certmesh SetHook, got: {other:?}"),
}
}
#[test]
fn parse_certmesh_create_with_all_options() {
let cli = Cli::try_parse_from([
"koi",
"certmesh",
"create",
"--profile",
"team",
"--operator",
"ops",
"--enrollment",
"open",
"--require-approval",
"true",
"--passphrase",
"my-secret",
])
.unwrap();
match cli.command {
Some(Command::Certmesh(CertmeshCommand {
command:
Some(CertmeshSubcommand::Create {
profile,
operator,
enrollment,
require_approval,
passphrase,
}),
})) => {
assert_eq!(profile.as_deref(), Some("team"));
assert_eq!(operator.as_deref(), Some("ops"));
assert_eq!(enrollment.as_deref(), Some("open"));
assert_eq!(require_approval, Some(true));
assert_eq!(passphrase.as_deref(), Some("my-secret"));
}
other => panic!("Expected Certmesh Create, got: {other:?}"),
}
}
#[test]
fn parse_global_json_flag() {
let cli = Cli::try_parse_from(["koi", "--json", "certmesh", "status"]).unwrap();
assert!(cli.json);
}
#[test]
fn parse_global_endpoint_flag() {
let cli = Cli::try_parse_from([
"koi",
"--endpoint",
"http://localhost:5641",
"certmesh",
"status",
])
.unwrap();
assert_eq!(cli.endpoint.as_deref(), Some("http://localhost:5641"));
}
#[test]
fn parse_mdns_discover_no_type() {
let cli = Cli::try_parse_from(["koi", "mdns", "discover"]).unwrap();
match cli.command {
Some(Command::Mdns(MdnsCommand {
command: Some(MdnsSubcommand::Discover { service_type }),
})) => {
assert!(service_type.is_none());
}
other => panic!("Expected Mdns Discover, got: {other:?}"),
}
}
#[test]
fn parse_mdns_discover_with_type() {
let cli = Cli::try_parse_from(["koi", "mdns", "discover", "_http._tcp"]).unwrap();
match cli.command {
Some(Command::Mdns(MdnsCommand {
command: Some(MdnsSubcommand::Discover { service_type }),
})) => {
assert_eq!(service_type.as_deref(), Some("_http._tcp"));
}
other => panic!("Expected Mdns Discover, got: {other:?}"),
}
}
#[test]
fn parse_mdns_announce_all_args() {
let cli = Cli::try_parse_from([
"koi",
"mdns",
"announce",
"My App",
"_http._tcp",
"8080",
"--ip",
"10.0.0.1",
"version=1.0",
"env=prod",
])
.unwrap();
match cli.command {
Some(Command::Mdns(MdnsCommand {
command:
Some(MdnsSubcommand::Announce {
name,
service_type,
port,
ip,
txt,
}),
})) => {
assert_eq!(name, "My App");
assert_eq!(service_type, "_http._tcp");
assert_eq!(port, 8080);
assert_eq!(ip.as_deref(), Some("10.0.0.1"));
assert_eq!(txt, vec!["version=1.0", "env=prod"]);
}
other => panic!("Expected Mdns Announce, got: {other:?}"),
}
}
#[test]
fn parse_mdns_announce_minimal() {
let cli =
Cli::try_parse_from(["koi", "mdns", "announce", "Svc", "_ssh._tcp", "22"]).unwrap();
match cli.command {
Some(Command::Mdns(MdnsCommand {
command:
Some(MdnsSubcommand::Announce {
name,
service_type,
port,
ip,
txt,
}),
})) => {
assert_eq!(name, "Svc");
assert_eq!(service_type, "_ssh._tcp");
assert_eq!(port, 22);
assert!(ip.is_none());
assert!(txt.is_empty());
}
other => panic!("Expected Mdns Announce, got: {other:?}"),
}
}
#[test]
fn parse_mdns_unregister() {
let cli = Cli::try_parse_from(["koi", "mdns", "unregister", "abc12345"]).unwrap();
match cli.command {
Some(Command::Mdns(MdnsCommand {
command: Some(MdnsSubcommand::Unregister { id }),
})) => {
assert_eq!(id, "abc12345");
}
other => panic!("Expected Mdns Unregister, got: {other:?}"),
}
}
#[test]
fn parse_mdns_resolve() {
let cli =
Cli::try_parse_from(["koi", "mdns", "resolve", "My Server._http._tcp.local."]).unwrap();
match cli.command {
Some(Command::Mdns(MdnsCommand {
command: Some(MdnsSubcommand::Resolve { instance }),
})) => {
assert_eq!(instance, "My Server._http._tcp.local.");
}
other => panic!("Expected Mdns Resolve, got: {other:?}"),
}
}
#[test]
fn parse_mdns_subscribe() {
let cli = Cli::try_parse_from(["koi", "mdns", "subscribe", "_http._tcp"]).unwrap();
match cli.command {
Some(Command::Mdns(MdnsCommand {
command: Some(MdnsSubcommand::Subscribe { service_type }),
})) => {
assert_eq!(service_type, "_http._tcp");
}
other => panic!("Expected Mdns Subscribe, got: {other:?}"),
}
}
#[test]
fn parse_mdns_admin_status() {
let cli = Cli::try_parse_from(["koi", "mdns", "admin", "status"]).unwrap();
match cli.command {
Some(Command::Mdns(MdnsCommand {
command:
Some(MdnsSubcommand::Admin(MdnsAdminCommand {
command: Some(AdminSubcommand::Status),
})),
})) => {}
other => panic!("Expected Admin Status, got: {other:?}"),
}
}
#[test]
fn parse_mdns_admin_ls() {
let cli = Cli::try_parse_from(["koi", "mdns", "admin", "ls"]).unwrap();
match cli.command {
Some(Command::Mdns(MdnsCommand {
command:
Some(MdnsSubcommand::Admin(MdnsAdminCommand {
command: Some(AdminSubcommand::List),
})),
})) => {}
other => panic!("Expected Admin List, got: {other:?}"),
}
}
#[test]
fn parse_mdns_admin_inspect() {
let cli = Cli::try_parse_from(["koi", "mdns", "admin", "inspect", "a1b2c3"]).unwrap();
match cli.command {
Some(Command::Mdns(MdnsCommand {
command:
Some(MdnsSubcommand::Admin(MdnsAdminCommand {
command: Some(AdminSubcommand::Inspect { id }),
})),
})) => {
assert_eq!(id, "a1b2c3");
}
other => panic!("Expected Admin Inspect, got: {other:?}"),
}
}
#[test]
fn parse_mdns_admin_unregister() {
let cli = Cli::try_parse_from(["koi", "mdns", "admin", "unregister", "xyz"]).unwrap();
match cli.command {
Some(Command::Mdns(MdnsCommand {
command:
Some(MdnsSubcommand::Admin(MdnsAdminCommand {
command: Some(AdminSubcommand::Unregister { id }),
})),
})) => {
assert_eq!(id, "xyz");
}
other => panic!("Expected Admin Unregister, got: {other:?}"),
}
}
#[test]
fn parse_mdns_admin_drain() {
let cli = Cli::try_parse_from(["koi", "mdns", "admin", "drain", "abc"]).unwrap();
match cli.command {
Some(Command::Mdns(MdnsCommand {
command:
Some(MdnsSubcommand::Admin(MdnsAdminCommand {
command: Some(AdminSubcommand::Drain { id }),
})),
})) => {
assert_eq!(id, "abc");
}
other => panic!("Expected Admin Drain, got: {other:?}"),
}
}
#[test]
fn parse_mdns_admin_revive() {
let cli = Cli::try_parse_from(["koi", "mdns", "admin", "revive", "def"]).unwrap();
match cli.command {
Some(Command::Mdns(MdnsCommand {
command:
Some(MdnsSubcommand::Admin(MdnsAdminCommand {
command: Some(AdminSubcommand::Revive { id }),
})),
})) => {
assert_eq!(id, "def");
}
other => panic!("Expected Admin Revive, got: {other:?}"),
}
}
#[test]
fn parse_mdns_with_timeout() {
let cli = Cli::try_parse_from(["koi", "--timeout", "30", "mdns", "discover"]).unwrap();
assert_eq!(cli.timeout, Some(30));
}
#[test]
fn parse_mdns_with_json_flag() {
let cli =
Cli::try_parse_from(["koi", "--json", "mdns", "subscribe", "_http._tcp"]).unwrap();
assert!(cli.json);
}
#[test]
fn parse_mdns_with_verbose() {
let cli = Cli::try_parse_from(["koi", "-vv", "mdns", "discover"]).unwrap();
assert_eq!(cli.verbose, 2);
}
#[test]
fn parse_daemon_flag() {
let cli = Cli::try_parse_from(["koi", "--daemon"]).unwrap();
assert!(cli.daemon);
}
#[test]
fn parse_no_subcommand() {
let cli = Cli::try_parse_from(["koi"]).unwrap();
assert!(cli.command.is_none());
}
#[test]
fn parse_certmesh_no_subcommand() {
let cli = Cli::try_parse_from(["koi", "certmesh"]).unwrap();
match cli.command {
Some(Command::Certmesh(CertmeshCommand { command: None })) => {}
other => panic!("Expected Certmesh without subcommand, got: {other:?}"),
}
}
#[test]
fn parse_mdns_no_subcommand() {
let cli = Cli::try_parse_from(["koi", "mdns"]).unwrap();
match cli.command {
Some(Command::Mdns(MdnsCommand { command: None })) => {}
other => panic!("Expected Mdns without subcommand, got: {other:?}"),
}
}
#[test]
fn parse_mdns_admin_no_subcommand() {
let cli = Cli::try_parse_from(["koi", "mdns", "admin"]).unwrap();
match cli.command {
Some(Command::Mdns(MdnsCommand {
command: Some(MdnsSubcommand::Admin(MdnsAdminCommand { command: None })),
})) => {}
other => panic!("Expected Mdns admin without subcommand, got: {other:?}"),
}
}
#[test]
fn parse_version_subcommand() {
let cli = Cli::try_parse_from(["koi", "version"]).unwrap();
assert!(matches!(cli.command, Some(Command::Version)));
}
#[test]
fn parse_status_subcommand() {
let cli = Cli::try_parse_from(["koi", "status"]).unwrap();
assert!(matches!(cli.command, Some(Command::Status)));
}
#[test]
fn parse_install_subcommand() {
let cli = Cli::try_parse_from(["koi", "install"]).unwrap();
assert!(matches!(cli.command, Some(Command::Install)));
}
#[test]
fn parse_standalone_flag() {
let cli = Cli::try_parse_from(["koi", "--standalone", "mdns", "discover"]).unwrap();
assert!(cli.standalone);
}
#[test]
fn require_capability_error_message_includes_env_var_hint() {
let config = Config {
no_mdns: true,
..Config::default()
};
let msg = config.require_capability("mdns").unwrap_err().to_string();
assert!(
msg.contains("KOI_NO_MDNS"),
"error should mention env var: {msg}"
);
}
#[test]
fn require_capability_mdns_enabled_certmesh_disabled_mdns_works() {
let config = Config {
no_certmesh: true,
..Config::default()
};
assert!(config.require_capability("mdns").is_ok());
assert!(config.require_capability("certmesh").is_err());
}
#[test]
fn config_default_pipe_path_is_not_empty() {
let config = Config::default();
assert!(
config.pipe_path.components().count() > 0,
"pipe path should not be empty"
);
}
#[test]
fn parse_certmesh_open_enrollment() {
let cli = Cli::try_parse_from(["koi", "certmesh", "open-enrollment"]).unwrap();
match cli.command {
Some(Command::Certmesh(CertmeshCommand {
command: Some(CertmeshSubcommand::OpenEnrollment { until }),
})) => {
assert!(until.is_none());
}
other => panic!("Expected OpenEnrollment, got: {other:?}"),
}
}
#[test]
fn parse_certmesh_open_enrollment_with_deadline() {
let cli =
Cli::try_parse_from(["koi", "certmesh", "open-enrollment", "--until", "2h"]).unwrap();
match cli.command {
Some(Command::Certmesh(CertmeshCommand {
command: Some(CertmeshSubcommand::OpenEnrollment { until }),
})) => {
assert_eq!(until.as_deref(), Some("2h"));
}
other => panic!("Expected OpenEnrollment, got: {other:?}"),
}
}
#[test]
fn parse_certmesh_close_enrollment() {
let cli = Cli::try_parse_from(["koi", "certmesh", "close-enrollment"]).unwrap();
match cli.command {
Some(Command::Certmesh(CertmeshCommand {
command: Some(CertmeshSubcommand::CloseEnrollment),
})) => {}
other => panic!("Expected CloseEnrollment, got: {other:?}"),
}
}
#[test]
fn parse_certmesh_set_policy_domain() {
let cli = Cli::try_parse_from(["koi", "certmesh", "set-policy", "--domain", "lab.local"])
.unwrap();
match cli.command {
Some(Command::Certmesh(CertmeshCommand {
command:
Some(CertmeshSubcommand::SetPolicy {
domain,
subnet,
clear,
}),
})) => {
assert_eq!(domain.as_deref(), Some("lab.local"));
assert!(subnet.is_none());
assert!(!clear);
}
other => panic!("Expected SetPolicy, got: {other:?}"),
}
}
#[test]
fn parse_certmesh_set_policy_subnet() {
let cli = Cli::try_parse_from([
"koi",
"certmesh",
"set-policy",
"--subnet",
"192.168.1.0/24",
])
.unwrap();
match cli.command {
Some(Command::Certmesh(CertmeshCommand {
command:
Some(CertmeshSubcommand::SetPolicy {
domain,
subnet,
clear,
}),
})) => {
assert!(domain.is_none());
assert_eq!(subnet.as_deref(), Some("192.168.1.0/24"));
assert!(!clear);
}
other => panic!("Expected SetPolicy, got: {other:?}"),
}
}
#[test]
fn parse_certmesh_set_policy_clear() {
let cli = Cli::try_parse_from(["koi", "certmesh", "set-policy", "--clear"]).unwrap();
match cli.command {
Some(Command::Certmesh(CertmeshCommand {
command:
Some(CertmeshSubcommand::SetPolicy {
domain,
subnet,
clear,
}),
})) => {
assert!(domain.is_none());
assert!(subnet.is_none());
assert!(clear);
}
other => panic!("Expected SetPolicy, got: {other:?}"),
}
}
#[test]
fn parse_certmesh_set_policy_both() {
let cli = Cli::try_parse_from([
"koi",
"certmesh",
"set-policy",
"--domain",
"lab.local",
"--subnet",
"10.0.0.0/8",
])
.unwrap();
match cli.command {
Some(Command::Certmesh(CertmeshCommand {
command:
Some(CertmeshSubcommand::SetPolicy {
domain,
subnet,
clear,
}),
})) => {
assert_eq!(domain.as_deref(), Some("lab.local"));
assert_eq!(subnet.as_deref(), Some("10.0.0.0/8"));
assert!(!clear);
}
other => panic!("Expected SetPolicy, got: {other:?}"),
}
}
#[test]
fn parse_certmesh_rotate_auth() {
let cli = Cli::try_parse_from(["koi", "certmesh", "rotate-auth"]).unwrap();
match cli.command {
Some(Command::Certmesh(CertmeshCommand {
command: Some(CertmeshSubcommand::RotateAuth),
})) => {}
other => panic!("Expected RotateAuth, got: {other:?}"),
}
}
#[test]
fn parse_certmesh_destroy() {
let cli = Cli::try_parse_from(["koi", "certmesh", "destroy"]).unwrap();
match cli.command {
Some(Command::Certmesh(CertmeshCommand {
command: Some(CertmeshSubcommand::Destroy),
})) => {}
other => panic!("Expected Destroy, got: {other:?}"),
}
}
#[test]
fn test_msrv_drift_guard() {
use std::path::PathBuf;
let cargo_toml_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("Cargo.toml");
let cargo_contents =
std::fs::read_to_string(&cargo_toml_path).expect("failed to read Cargo.toml");
let rust_version = cargo_contents
.lines()
.find_map(|line| {
let trimmed = line.trim();
if trimmed.starts_with("rust-version") {
let parts: Vec<&str> = trimmed.split('=').collect();
if parts.len() == 2 {
let ver = parts[1]
.trim()
.trim_matches('"')
.trim_matches('\'')
.to_string();
return Some(ver);
}
}
None
})
.expect("Could not find rust-version in Cargo.toml");
let readme_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("README.md");
let readme_contents =
std::fs::read_to_string(&readme_path).expect("failed to read README.md");
let readme_version = readme_contents
.lines()
.find_map(|line| {
if line.contains("requires [Rust](https://rustup.rs/)") {
let prefix = "requires [Rust](https://rustup.rs/) ";
if let Some(idx) = line.find(prefix) {
let start = idx + prefix.len();
let rest = &line[start..];
let ver = rest.split_whitespace().next().unwrap_or("");
return Some(ver.to_string());
}
}
None
})
.expect("Could not find Rust MSRV reference in README.md");
assert_eq!(
rust_version, readme_version,
"MSRV in Cargo.toml ({}) does not match README.md ({})",
rust_version, readme_version
);
}
}