manta-cli 2.0.0-beta.62

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`.
//!
//! ## Boot sequence
//!
//! 1. [`build::build_cli`] constructs the clap tree and parses
//!    `std::env::args` into `clap::ArgMatches`.
//! 2. [`manta_shared::common::config::get_cli_configuration`] loads
//!    `~/.config/manta/cli.toml`, then [`common::config::CliConfiguration`]
//!    typed-deserialises it.
//! 3. The active site is resolved (`--site` flag wins over
//!    `cli.toml`'s `site`); the SOCKS5 env var is set while still
//!    single-threaded.
//! 4. A multi-threaded tokio runtime is built and `run_cli` is the
//!    entry future.
//! 5. Tracing is configured, the [`common::app_context::AppContext`]
//!    is assembled, and [`dispatch::process::process_cli`] routes
//!    based on the parsed verb.
//!
//! `dispatch::*`, `output::*`, and `http_client::*` are documented in
//! their respective modules. The `openapi_client` sibling module is
//! progenitor-generated at build time from `openapi.json`.

#![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). Left `None` when neither `--site` nor `cli.toml`'s
  // `site` is set; commands that reach the server raise the error
  // lazily via `AppContext::require_site`, so purely-local config
  // commands (e.g. `config set site`) still work with no site set.
  // 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: Option<String> = cli_matches
    .get_one::<String>("site")
    .cloned()
    .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 (`None` when unset) and
/// forwards it on every server request via the `X-Manta-Site` header.
/// Commands that need it but find `None` fail via `require_site`.
async fn run_cli(
  settings: config::Config,
  configuration: CliConfiguration,
  site_name: Option<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.as_deref(),
    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,
    read_only: configuration.read_only,
    settings: &settings,
    token: None,
    session: None,
  };

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

  cli_result.map_err(Into::into)
}