use ::manta_backend_dispatcher::types::K8sAuth;
use manta_shared::common::{config as manta_config, log_ops};
use manta_server::config::{BackendTechnology, ServerConfiguration};
use manta_server::dispatcher::StaticBackendDispatcher;
use manta_server::server;
const API_URL_SUFFIX: &str = "/apis";
const VCS_URL_SUFFIX: &str = "/vcs";
const HELP: &str = "\
Manta HTTPS server — proxies CLI requests to CSM/Ochami backends.
Usage: manta-server [OPTIONS]
Options:
--port <PORT> Override [server] port from server.toml.
--cert <CERT> Override [server] cert from server.toml.
--key <KEY> Override [server] key from server.toml.
--listen-address <ADDR> Override [server] listen_address from server.toml.
--allow-http Allow listening over plain HTTP when no cert/key is
set. Use only when TLS terminates upstream (reverse
proxy, sidecar).
--emit-openapi Dump the OpenAPI spec to stdout as JSON and exit.
Used to regenerate crates/manta-cli/openapi.json
after handler or schema changes — no config file is
read.
-h, --help Print help.
-V, --version Print version.
";
#[derive(Default)]
struct CliArgs {
port: Option<u16>,
cert: Option<String>,
key: Option<String>,
listen_address: Option<String>,
allow_http: bool,
emit_openapi: bool,
}
fn parse_cli_args() -> Result<CliArgs, String> {
let mut args = std::env::args().skip(1);
let mut out = CliArgs::default();
let take_value = |inline: Option<String>,
rest: &mut std::iter::Skip<std::env::Args>,
flag: &str|
-> Result<String, String> {
if let Some(v) = inline {
Ok(v)
} else {
rest
.next()
.ok_or_else(|| format!("{flag} requires a value"))
}
};
while let Some(raw) = args.next() {
let (name, inline) = match raw.split_once('=') {
Some((n, v)) => (n.to_string(), Some(v.to_string())),
None => (raw, None),
};
match name.as_str() {
"--help" | "-h" => {
print!("{HELP}");
std::process::exit(0);
}
"--version" | "-V" => {
println!("manta-server {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
"--port" => {
let v = take_value(inline, &mut args, "--port")?;
out.port = Some(
v.parse::<u16>()
.map_err(|e| format!("--port: invalid u16 '{v}': {e}"))?,
);
}
"--cert" => out.cert = Some(take_value(inline, &mut args, "--cert")?),
"--key" => out.key = Some(take_value(inline, &mut args, "--key")?),
"--listen-address" => {
out.listen_address =
Some(take_value(inline, &mut args, "--listen-address")?);
}
"--allow-http" => out.allow_http = true,
"--emit-openapi" => out.emit_openapi = true,
other => return Err(format!("unknown argument: {other}")),
}
}
Ok(out)
}
fn print_startup_summary(
configuration: &ServerConfiguration,
listen_addr: &str,
port: u16,
cert_path: &Option<String>,
key_path: &Option<String>,
) {
let (config_path, source) = match manta_config::get_server_config_file_path()
{
Ok(p) => (
p.display().to_string(),
if std::env::var("MANTA_SERVER_CONFIG").is_ok() {
"MANTA_SERVER_CONFIG env var"
} else {
"default lookup (~/.config/manta/server.toml)"
},
),
Err(_) => ("<unknown>".to_string(), "unresolved"),
};
println!("manta-server configuration");
println!("==========================");
println!("config file: {config_path}");
println!("source: {source}");
println!();
println!("[server]");
println!(" listen_address: {listen_addr}");
println!(" port: {port}");
println!(
" cert: {}",
cert_path.as_deref().unwrap_or("<none>")
);
println!(
" key: {}",
key_path.as_deref().map_or("<none>", |_| "<set>")
);
println!(
" console_inactivity_timeout_secs: {}",
configuration.server.console_inactivity_timeout_secs
);
println!(
" auth_rate_limit_per_minute: {}",
configuration
.server
.auth_rate_limit_per_minute
.map_or_else(|| "<disabled>".to_string(), |n| n.to_string())
);
println!(" log_filter: {}", configuration.log);
println!();
println!("[auditor]");
match configuration.auditor.as_ref() {
Some(a) => {
println!(" Kafka audit forwarder enabled");
println!(" brokers: {:?}", a.kafka.brokers);
println!(" topic: {}", a.kafka.topic);
}
None => println!(" disabled (no audit messages will be emitted)"),
}
println!();
}
#[allow(clippy::too_many_arguments)]
fn print_site_summary(
name: &str,
backend: &str,
shasta_base_url: &str,
gitea_base_url: &str,
k8s_api_url: Option<&str>,
vault_base_url: Option<&str>,
has_socks5_proxy: bool,
root_ca_cert_file: &str,
) {
println!("[site: {name}]");
println!(" backend: {backend}");
println!(" shasta_base_url: {shasta_base_url}");
println!(" gitea_base_url: {gitea_base_url}");
println!(" k8s_api_url: {}", k8s_api_url.unwrap_or("<none>"));
println!(
" vault_base_url: {}",
vault_base_url.unwrap_or("<none>")
);
println!(
" socks5_proxy: {}",
if has_socks5_proxy { "<set>" } else { "<none>" }
);
println!(" root_ca_cert_file: {root_ca_cert_file}");
println!();
}
fn main() {
if let Err(e) = run() {
eprintln!("{e}");
std::process::exit(1);
}
}
fn run() -> core::result::Result<(), Box<dyn std::error::Error>> {
rustls::crypto::ring::default_provider()
.install_default()
.ok();
let cli =
parse_cli_args().map_err(|e| format!("{e}\nRun with --help for usage."))?;
if cli.emit_openapi {
use utoipa::OpenApi;
let spec = manta_server::server::api_doc::ApiDoc::openapi()
.to_pretty_json()
.map_err(|e| format!("Failed to serialise OpenAPI spec: {e}"))?;
println!("{spec}");
return Ok(());
}
let settings = manta_config::get_server_configuration()
.map_err(|e| format!("Could not read server configuration: {e}"))?;
let configuration: ServerConfiguration = settings
.try_deserialize()
.map_err(|e| format!("Server configuration file is not valid: {e}"))?;
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(run_server(configuration, cli))
}
async fn run_server(
configuration: ServerConfiguration,
cli: CliArgs,
) -> core::result::Result<(), Box<dyn std::error::Error>> {
log_ops::configure(&configuration.log, true);
let cert_path: Option<String> =
cli.cert.or_else(|| configuration.server.cert.clone());
let key_path: Option<String> =
cli.key.or_else(|| configuration.server.key.clone());
let has_tls = cert_path.is_some() && key_path.is_some();
let allow_http = cli.allow_http || configuration.server.allow_http;
if !has_tls && !allow_http {
return Err(
"Refusing to start without TLS: configure `cert` + `key` in \
[server] (or pass --cert/--key), or set `allow_http = true` / \
pass --allow-http if TLS terminates upstream. The default \
is fail-closed so bearer tokens cannot accidentally land on \
the wire in cleartext."
.into(),
);
}
let port: u16 = cli.port.or(configuration.server.port).unwrap_or_else(|| {
manta_server::config::ServerSettings::default_port(has_tls)
});
let listen_addr: String = cli
.listen_address
.or_else(|| configuration.server.listen_address.clone())
.unwrap_or_else(|| {
manta_server::config::ServerSettings::DEFAULT_LISTEN_ADDRESS.to_string()
});
let console_inactivity_timeout = std::time::Duration::from_secs(
configuration.server.console_inactivity_timeout_secs,
);
let request_timeout =
std::time::Duration::from_secs(configuration.server.request_timeout_secs);
let migrate_backup_root = match configuration
.server
.migrate_backup_root
.as_deref()
{
Some(raw) => Some(
std::path::PathBuf::from(raw).canonicalize().map_err(|e| {
format!(
"[server] migrate_backup_root '{raw}' could not be canonicalised: {e}. \
Either point it at an existing directory or remove the entry to keep \
the /migrate/* endpoints disabled."
)
})?,
),
None => None,
};
print_startup_summary(
&configuration,
&listen_addr,
port,
&cert_path,
&key_path,
);
let mut sites = std::collections::HashMap::new();
for (name, site) in &configuration.sites {
let barebone = site
.shasta_base_url
.strip_suffix(API_URL_SUFFIX)
.unwrap_or(&site.shasta_base_url);
let api_url = match &site.backend {
BackendTechnology::Csm => barebone.to_owned() + API_URL_SUFFIX,
BackendTechnology::Ochami => barebone.to_owned(),
};
let gitea = barebone.to_owned() + VCS_URL_SUFFIX;
let k8s_url = site.k8s.as_ref().map(|k| k.api_url.clone());
let vault_url = site.k8s.as_ref().and_then(|k| match &k.authentication {
K8sAuth::Vault { base_url, .. } => Some(base_url.clone()),
K8sAuth::Native { .. } => None,
});
let root_cert =
manta_config::get_csm_root_cert_content(&site.root_ca_cert_file)
.map_err(|e| {
format!(
"CA cert for site '{name}' at '{}' could not be read: {e}. \
Fix the path under [sites.{name}].root_ca_cert_file in \
server.toml or remove the site entry.",
site.root_ca_cert_file
)
})?;
print_site_summary(
name,
site.backend.as_str(),
&api_url,
&gitea,
k8s_url.as_deref(),
vault_url.as_deref(),
site.socks5_proxy.is_some(),
&site.root_ca_cert_file,
);
let site_backend_dispatcher = StaticBackendDispatcher::new(
site.backend.as_str(),
&api_url,
&root_cert,
site.socks5_proxy.as_deref(),
)?;
sites.insert(
name.clone(),
server::SiteBackend {
backend: site_backend_dispatcher,
shasta_base_url: api_url,
shasta_root_cert: root_cert,
socks5_proxy: site.socks5_proxy.clone(),
vault_base_url: vault_url,
gitea_base_url: gitea,
k8s_api_url: k8s_url,
},
);
}
let auditor = configuration.auditor.as_ref().map(|a| a.kafka.clone());
let shutdown_grace_period = std::time::Duration::from_secs(
configuration.server.shutdown_grace_period_secs,
);
let server_state = std::sync::Arc::new(server::ServerState {
sites,
console_inactivity_timeout,
auditor,
auth_rate_limit_per_minute: configuration.server.auth_rate_limit_per_minute,
request_timeout,
shutdown_grace_period,
migrate_backup_root,
});
server::start_server(
server_state,
&listen_addr,
port,
cert_path.as_deref(),
key_path.as_deref(),
)
.await
.map_err(std::convert::Into::into)
}