manta-cli 2.0.0-beta.57

Another CLI for ALPS
//! Application entry point: parses CLI args, loads configuration, and
//! launches the CLI command handler. The CLI never talks to CSM /
//! OCHAMI directly — every operation is forwarded to the manta HTTPS
//! server named by `cli.toml`'s `manta_server_url`.

#![warn(missing_docs)]

mod build;
mod common;
mod dispatch;
mod http_client;
// The OpenAPI client is auto-generated by progenitor from the
// manta-server-emitted spec, so we can't add docs to its items in
// the source. The server-side handler docstrings that get embedded
// into the OpenAPI `description` fields sometimes include rustdoc
// intra-doc links like `[crate::service::session::...]` — those
// resolve in `manta-server` but not in the CLI's module tree where
// the generated client lands, so we mute the link lint here too.
#[allow(missing_docs, rustdoc::broken_intra_doc_links)]
mod openapi_client;
mod output;

use crate::common::app_context::AppContext;
use crate::common::config::CliConfiguration;

use clap::ArgMatches;

use manta_shared::common::log_ops;

/// Process entry point. Delegates to `run` and prints any error with
/// `Display` (not `Debug`) so multi-line messages aren't escaped.
fn main() {
  if let Err(e) = run() {
    eprintln!("{e}");
    std::process::exit(1);
  }
}

/// Synchronous entry point. Loads `cli.toml`, resolves the active site,
/// sets the SOCKS5 env var (must happen before the multi-threaded tokio
/// runtime is active), and then launches the async runtime.
fn run() -> core::result::Result<(), Box<dyn std::error::Error>> {
  let cli_matches = crate::build::build_cli().get_matches();

  let settings = manta_shared::common::config::get_cli_configuration()
    .map_err(|e| format!("Could not read CLI configuration file: {e}"))?;
  let configuration: CliConfiguration = settings
    .clone()
    .try_deserialize()
    .map_err(|e| format!("CLI configuration file is not valid: {e}"))?;

  let rt = tokio::runtime::Builder::new_multi_thread()
    .enable_all()
    .build()?;

  // Resolve the active site name (just a header value — the server
  // validates it). Set the SOCKS5 proxy env var while we are still
  // single-threaded; the proxy is used to reach manta-server, not the
  // backends — per-site backend proxying is the server's concern.
  let site_name: String = cli_matches
    .get_one::<String>("site")
    .cloned()
    .unwrap_or_else(|| configuration.site.clone());

  if let Some(socks_proxy) = &configuration.socks5_proxy
    && !socks_proxy.is_empty()
  {
    // SAFETY: no other threads are running yet.
    unsafe {
      std::env::set_var("SOCKS5", socks_proxy);
    }
  }

  rt.block_on(run_cli(settings, configuration, site_name, cli_matches))
}

/// CLI startup — takes the resolved site name and forwards it on every
/// request via the `X-Manta-Site` header.
async fn run_cli(
  settings: config::Config,
  configuration: CliConfiguration,
  site_name: String,
  cli_matches: ArgMatches,
) -> core::result::Result<(), Box<dyn std::error::Error>> {
  let log_level = settings
    .get_string("log")
    .unwrap_or_else(|_| "error".to_string());
  log_ops::configure(&log_level, false);

  if let Some(socks_proxy) = &configuration.socks5_proxy {
    if !socks_proxy.is_empty() {
      tracing::info!("SOCKS5 enabled: {:?}", std::env::var("SOCKS5"));
    } else {
      tracing::debug!("config - socks5_proxy: not defined");
    }
  }

  let settings_hsm_group_name_opt = settings.get_string("hsm_group").ok();
  let manta_server_url = configuration.manta_server_url.as_str();

  let app_context = AppContext {
    site_name: &site_name,
    manta_server_url,
    settings_group_name_opt: settings_hsm_group_name_opt.as_deref(),
    request_timeout_secs: configuration.request_timeout_secs,
    power_poll_interval_secs: configuration.power_poll_interval_secs,
    power_max_poll_attempts: configuration.power_max_poll_attempts,
    sat_file_poll_interval_secs: configuration.sat_file_poll_interval_secs,
    sat_file_poll_budget_secs: configuration.sat_file_poll_budget_secs,
    sat_file_not_visible_budget_secs: configuration
      .sat_file_not_visible_budget_secs,
    settings: &settings,
    read_only: configuration.read_only,
  };

  let cli_result =
    crate::dispatch::process::process_cli(&cli_matches, &app_context).await;

  cli_result.map_err(Into::into)
}