use std::io::IsTerminal;
use std::sync::Arc;
use anyhow::Context;
use anyhow::Result;
use clap::Parser;
use clap_verbosity_flag::Verbosity;
use clap_verbosity_flag::WarnLevel;
use planetary_api::Server;
use planetary_db::Database;
use planetary_server::DEFAULT_ADDRESS;
use planetary_server::DEFAULT_PORT;
use secrecy::SecretString;
use tes::v1::types::responses::ServiceInfo;
use tes::v1::types::responses::service_info;
use tracing_log::AsTrace as _;
use tracing_subscriber::EnvFilter;
use url::Url;
#[derive(Parser)]
pub struct ServiceInfoArgs {
#[clap(long, default_value = "org.example.planetary")]
server_identifier: String,
#[clap(long, default_value = "Planetary Server")]
server_name: String,
#[clap(long)]
description: Option<String>,
#[clap(long, default_value = "Example Organization")]
organization_name: String,
#[clap(long, default_value = "https://example.com")]
organization_url: Url,
#[clap(long)]
contact_url: Option<String>,
#[clap(long)]
documentation_url: Option<Url>,
#[clap(long)]
environment: Option<String>,
#[clap(long)]
server_version: Option<String>,
#[clap(long)]
storage: Option<Vec<String>>,
}
impl TryFrom<ServiceInfoArgs> for ServiceInfo {
type Error = anyhow::Error;
fn try_from(args: ServiceInfoArgs) -> Result<Self> {
let mut builder = service_info::Builder::default()
.id(args.server_identifier)
.name(args.server_name)
.org_name(args.organization_name)
.org_url(args.organization_url)
.version(
args.server_version
.unwrap_or_else(|| env!("CARGO_PKG_VERSION").into()),
);
if let Some(description) = args.description {
builder = builder.description(description);
}
if let Some(contact_url) = args.contact_url {
builder = builder.contact_url(contact_url);
}
if let Some(documentation_url) = args.documentation_url {
builder = builder.documentation_url(documentation_url);
}
if let Some(environment) = args.environment {
builder = builder.environment(environment);
}
if let Some(storage) = args.storage {
builder = builder.storage(storage);
}
builder
.try_build()
.context("building service info response")
}
}
#[derive(Parser)]
pub struct Args {
#[clap(short, long, default_value = DEFAULT_ADDRESS)]
address: String,
#[clap(short, long, default_value_t = DEFAULT_PORT)]
port: u16,
#[clap(flatten)]
service_info: ServiceInfoArgs,
#[command(flatten)]
verbose: Verbosity<WarnLevel>,
#[clap(long, env)]
pod_name: String,
#[clap(long, env)]
database_user: String,
#[clap(long, env, hide_env_values(true))]
database_password: SecretString,
#[clap(long, env)]
database_host: String,
#[clap(long, env, default_value_t = 5432)]
database_port: i32,
#[clap(long, env, default_value = "planetary")]
database_name: String,
#[clap(long, env)]
orchestrator_url: Url,
#[clap(long, env, hide_env_values(true))]
orchestrator_api_key: SecretString,
#[clap(long, env)]
allow_authorization_fallback: bool,
#[clap(long, env)]
allow_file_urls: bool,
}
impl Args {
async fn database(&self) -> Result<Arc<dyn Database>> {
cfg_if::cfg_if! {
if #[cfg(feature = "postgres")] {
let url = planetary_db::postgres::format_database_url(
&self.database_user,
&self.database_password,
&self.database_host,
self.database_port,
&self.database_name,
&self.pod_name,
);
Ok(Arc::new(planetary_db::postgres::PostgresDatabase::new(
url.into(),
)?))
} else {
compile_error!("no database feature was enabled");
}
}
}
}
#[cfg(unix)]
async fn terminate() {
use tokio::select;
use tokio::signal::unix::SignalKind;
use tokio::signal::unix::signal;
use tracing::info;
let mut sigterm = signal(SignalKind::terminate()).expect("failed to create SIGTERM handler");
let mut sigint = signal(SignalKind::interrupt()).expect("failed to create SIGINT handler");
let signal = select! {
_ = sigterm.recv() => "SIGTERM",
_ = sigint.recv() => "SIGINT",
};
info!("received {signal} signal: initiating shutdown");
}
#[cfg(windows)]
async fn terminate() {
use tokio::signal::windows::ctrl_c;
use tracing::info;
let mut signal = ctrl_c().expect("failed to create ctrl-c handler");
signal.await;
info!("received Ctrl-C signal: initiating shutdown");
}
#[tokio::main]
pub async fn main() -> anyhow::Result<()> {
let args = Args::parse();
match std::env::var("RUST_LOG") {
Ok(_) => tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_ansi(std::io::stdout().is_terminal())
.init(),
Err(_) => tracing_subscriber::fmt()
.with_max_level(args.verbose.log_level_filter().as_trace())
.with_ansi(std::io::stdout().is_terminal())
.init(),
}
let database = args.database().await?;
Server::builder()
.address(&args.address)
.port(args.port)
.shared_database(database)
.orchestrator_url(args.orchestrator_url)
.orchestrator_api_key(args.orchestrator_api_key)
.allow_authorization_fallback(args.allow_authorization_fallback)
.allow_file_urls(args.allow_file_urls)
.info(ServiceInfo::try_from(args.service_info)?)
.build()
.run(terminate())
.await
.context("failed to run server")
}