diff --git a/crates/ruvector-hailo-cluster/src/bin/stats.rs b/crates/ruvector-hailo-cluster/src/bin/stats.rs index 892df2882..58d6ea693 100644 --- a/crates/ruvector-hailo-cluster/src/bin/stats.rs +++ b/crates/ruvector-hailo-cluster/src/bin/stats.rs @@ -47,6 +47,17 @@ fn main() -> Result<(), Box> { // Useful for verifying a --workers-file manifest expands as expected, // or for resolving a tailscale tag → IPs without hitting the workers. let mut list_workers = false; + // Iter 189 — TLS / mTLS knobs (mirror of iter-187 bench + iter-188 + // embed). Lets ops snapshot fleet stats from a TLS-configured + // worker with the same flag surface as the other client tools. + #[cfg(feature = "tls")] + let mut tls_ca: Option = None; + #[cfg(feature = "tls")] + let mut tls_domain: Option = None; + #[cfg(feature = "tls")] + let mut tls_client_cert: Option = None; + #[cfg(feature = "tls")] + let mut tls_client_key: Option = None; let mut i = 1; while i < args.len() { @@ -82,6 +93,14 @@ fn main() -> Result<(), Box> { } "--strict-homogeneous" => { strict_homogeneous = true; i += 1; } "--list-workers" => { list_workers = true; i += 1; } + #[cfg(feature = "tls")] + "--tls-ca" => { tls_ca = args.get(i + 1).cloned(); i += 2; } + #[cfg(feature = "tls")] + "--tls-domain" => { tls_domain = args.get(i + 1).cloned(); i += 2; } + #[cfg(feature = "tls")] + "--tls-client-cert" => { tls_client_cert = args.get(i + 1).cloned(); i += 2; } + #[cfg(feature = "tls")] + "--tls-client-key" => { tls_client_key = args.get(i + 1).cloned(); i += 2; } "--help" | "-h" => { print_help(); return Ok(()); } "--version" | "-V" => { println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); @@ -154,6 +173,45 @@ fn main() -> Result<(), Box> { // Cluster is built solely to call fleet_stats — dim + fingerprint // values don't matter for that path. + // + // Iter 189 — TLS transport when --tls-ca is set. Same partial-config + // + orphan-flag refusals as iter-187/188 client tools. + #[cfg(feature = "tls")] + let transport = { + if let Some(ca_path) = tls_ca.as_deref() { + let addr0 = workers.first().map(|w| w.address.clone()).unwrap_or_default(); + let domain = tls_domain.clone().unwrap_or_else(|| { + ruvector_hailo_cluster::tls::domain_from_address(&addr0).to_string() + }); + let mut tls = ruvector_hailo_cluster::tls::TlsClient::from_pem_files(ca_path, &domain) + .map_err(|e| format!("--tls-ca: {}", e))?; + match (tls_client_cert.as_deref(), tls_client_key.as_deref()) { + (Some(c), Some(k)) => { + tls = tls.with_client_identity(c, k) + .map_err(|e| format!("--tls-client-cert/--tls-client-key: {}", e))?; + } + (Some(_), None) | (None, Some(_)) => { + return Err( + "--tls-client-cert and --tls-client-key must both be set or both unset".into(), + ); + } + (None, None) => {} + } + Arc::new(GrpcTransport::with_tls( + std::time::Duration::from_secs(5), + std::time::Duration::from_secs(2), + tls, + )?) + } else { + if tls_domain.is_some() || tls_client_cert.is_some() || tls_client_key.is_some() { + return Err( + "--tls-domain / --tls-client-cert / --tls-client-key require --tls-ca".into(), + ); + } + Arc::new(GrpcTransport::new()?) + } + }; + #[cfg(not(feature = "tls"))] let transport = Arc::new(GrpcTransport::new()?); let cluster = HailoClusterEmbedder::new(workers, transport, 1, "")?; @@ -570,6 +628,19 @@ OPTIONS: --workers-file manifest or resolving a tailscale tag without hitting workers. + --tls-ca Enable HTTPS by trusting the PEM CA + bundle at . Without this the + stats CLI dials plaintext gRPC. + (Requires --features tls.) + --tls-domain SNI / SAN value to assert against + the server cert. Defaults to the + hostname half of the first worker + address. + --tls-client-cert mTLS client cert (PEM). Pair with + --tls-client-key. + --tls-client-key mTLS client private key (PEM). Both + cert and key must be set or both + unset. --help, -h Print this help and exit. --version, -V Print the binary name + version and exit.