Files
cli/sunbeam-sdk/src/search/cli.rs
Sienna Meridian Satterwhite faf525522c feat: async SunbeamClient factory with unified auth resolution
SunbeamClient accessors are now async and resolve auth per-client:
- SSO bearer (get_token) for admin APIs, Matrix, La Suite, OpenSearch
- Gitea PAT (get_gitea_token) for VCS
- None for Prometheus, Loki, S3, LiveKit

Fixes client URLs to match deployed routes: hydra→hydra.{domain},
matrix→messages.{domain}, grafana→metrics.{domain},
prometheus→systemmetrics.{domain}, loki→systemlogs.{domain}.

Removes all ad-hoc token helpers from CLI modules (matrix_with_token,
os_client, people_client, etc). Every dispatch just calls
client.service().await?.
2026-03-22 18:57:22 +00:00

712 lines
20 KiB
Rust

//! CLI commands for OpenSearch.
use clap::Subcommand;
use serde_json::json;
use crate::error::Result;
use crate::output::{self, OutputFormat};
// ---------------------------------------------------------------------------
// Top-level command enum
// ---------------------------------------------------------------------------
#[derive(Subcommand, Debug)]
pub enum SearchCommand {
/// Document operations.
Doc {
#[command(subcommand)]
action: DocAction,
},
/// Search an index.
Query {
/// Index name.
#[arg(short = 'x', long)]
index: String,
/// Query string (wrapped in query_string query).
#[arg(short, long)]
query: Option<String>,
/// Raw JSON body (overrides --query).
#[arg(short, long)]
data: Option<String>,
},
/// Count documents in an index.
Count {
/// Index name.
#[arg(short = 'x', long)]
index: String,
/// Raw JSON body for the count query.
#[arg(short, long)]
data: Option<String>,
},
/// Index management.
Index {
#[command(subcommand)]
action: IndexAction,
},
/// Cluster operations.
Cluster {
#[command(subcommand)]
action: ClusterAction,
},
/// Node operations.
Node {
#[command(subcommand)]
action: NodeAction,
},
/// Ingest pipeline management.
Ingest {
#[command(subcommand)]
action: IngestAction,
},
/// Snapshot management.
Snapshot {
#[command(subcommand)]
action: SnapshotAction,
},
}
// ---------------------------------------------------------------------------
// Document
// ---------------------------------------------------------------------------
#[derive(Subcommand, Debug)]
pub enum DocAction {
/// Get a document by ID.
Get {
/// Index name.
#[arg(short = 'x', long)]
index: String,
/// Document ID.
#[arg(short, long)]
id: String,
},
/// Index (create/replace) a document.
Create {
/// Index name.
#[arg(short = 'x', long)]
index: String,
/// Document ID.
#[arg(short, long)]
id: String,
/// JSON body (or "-" for stdin).
#[arg(short, long)]
data: Option<String>,
},
/// Update a document by ID.
Update {
/// Index name.
#[arg(short = 'x', long)]
index: String,
/// Document ID.
#[arg(short, long)]
id: String,
/// JSON body (or "-" for stdin).
#[arg(short, long)]
data: Option<String>,
},
/// Delete a document by ID.
Delete {
/// Index name.
#[arg(short = 'x', long)]
index: String,
/// Document ID.
#[arg(short, long)]
id: String,
},
/// Check if a document exists.
Exists {
/// Index name.
#[arg(short = 'x', long)]
index: String,
/// Document ID.
#[arg(short, long)]
id: String,
},
}
// ---------------------------------------------------------------------------
// Index
// ---------------------------------------------------------------------------
#[derive(Subcommand, Debug)]
pub enum IndexAction {
/// List all indices.
List,
/// Create an index.
Create {
/// Index name.
#[arg(short = 'x', long)]
index: String,
/// JSON body (settings/mappings, or "-" for stdin).
#[arg(short, long)]
data: Option<String>,
},
/// Get index metadata.
Get {
/// Index name.
#[arg(short = 'x', long)]
index: String,
},
/// Delete an index.
Delete {
/// Index name.
#[arg(short = 'x', long)]
index: String,
},
/// Check if an index exists.
Exists {
/// Index name.
#[arg(short = 'x', long)]
index: String,
},
/// Open a closed index.
Open {
/// Index name.
#[arg(short = 'x', long)]
index: String,
},
/// Close an index.
Close {
/// Index name.
#[arg(short = 'x', long)]
index: String,
},
/// Index mapping operations.
Mapping {
#[command(subcommand)]
action: MappingAction,
},
/// Index settings operations.
Settings {
#[command(subcommand)]
action: SettingsAction,
},
}
#[derive(Subcommand, Debug)]
pub enum MappingAction {
/// Get index mapping.
Get {
/// Index name.
#[arg(short = 'x', long)]
index: String,
},
/// Update index mapping.
Update {
/// Index name.
#[arg(short = 'x', long)]
index: String,
/// JSON body (or "-" for stdin).
#[arg(short, long)]
data: Option<String>,
},
}
#[derive(Subcommand, Debug)]
pub enum SettingsAction {
/// Get index settings.
Get {
/// Index name.
#[arg(short = 'x', long)]
index: String,
},
/// Update index settings.
Update {
/// Index name.
#[arg(short = 'x', long)]
index: String,
/// JSON body (or "-" for stdin).
#[arg(short, long)]
data: Option<String>,
},
}
// ---------------------------------------------------------------------------
// Cluster
// ---------------------------------------------------------------------------
#[derive(Subcommand, Debug)]
pub enum ClusterAction {
/// Cluster health.
Health,
/// Cluster state.
State,
/// Cluster stats.
Stats,
}
// ---------------------------------------------------------------------------
// Node
// ---------------------------------------------------------------------------
#[derive(Subcommand, Debug)]
pub enum NodeAction {
/// List nodes.
List,
/// Node stats.
Stats,
/// Node info.
Info,
}
// ---------------------------------------------------------------------------
// Ingest
// ---------------------------------------------------------------------------
#[derive(Subcommand, Debug)]
pub enum IngestAction {
/// Ingest pipeline management.
Pipeline {
#[command(subcommand)]
action: PipelineAction,
},
}
#[derive(Subcommand, Debug)]
pub enum PipelineAction {
/// List all pipelines.
List,
/// Create or update a pipeline.
Create {
/// Pipeline ID.
#[arg(short, long)]
id: String,
/// JSON body (or "-" for stdin).
#[arg(short, long)]
data: Option<String>,
},
/// Get a pipeline.
Get {
/// Pipeline ID.
#[arg(short, long)]
id: String,
},
/// Delete a pipeline.
Delete {
/// Pipeline ID.
#[arg(short, long)]
id: String,
},
}
// ---------------------------------------------------------------------------
// Snapshot
// ---------------------------------------------------------------------------
#[derive(Subcommand, Debug)]
pub enum SnapshotAction {
/// List snapshots in a repository.
List {
/// Repository name.
#[arg(long)]
repo: String,
},
/// Create a snapshot.
Create {
/// Repository name.
#[arg(long)]
repo: String,
/// Snapshot name.
#[arg(long)]
name: String,
/// JSON body (or "-" for stdin).
#[arg(short, long)]
data: Option<String>,
},
/// Delete a snapshot.
Delete {
/// Repository name.
#[arg(long)]
repo: String,
/// Snapshot name.
#[arg(long)]
name: String,
},
/// Restore a snapshot.
Restore {
/// Repository name.
#[arg(long)]
repo: String,
/// Snapshot name.
#[arg(long)]
name: String,
/// JSON body (or "-" for stdin).
#[arg(short, long)]
data: Option<String>,
},
/// Snapshot repository management.
Repo {
#[command(subcommand)]
action: RepoAction,
},
}
#[derive(Subcommand, Debug)]
pub enum RepoAction {
/// Create or update a snapshot repository.
Create {
/// Repository name.
#[arg(long)]
name: String,
/// JSON body (or "-" for stdin).
#[arg(short, long)]
data: Option<String>,
},
/// Delete a snapshot repository.
Delete {
/// Repository name.
#[arg(long)]
name: String,
},
}
// ---------------------------------------------------------------------------
// Dispatch
// ---------------------------------------------------------------------------
pub async fn dispatch(
cmd: SearchCommand,
client: &crate::client::SunbeamClient,
fmt: OutputFormat,
) -> Result<()> {
let c = client.opensearch().await?;
match cmd {
// -----------------------------------------------------------------
// Documents
// -----------------------------------------------------------------
SearchCommand::Doc { action } => match action {
DocAction::Get { index, id } => {
let resp = c.get_doc(&index, &id).await?;
output::render(&resp, fmt)
}
DocAction::Create { index, id, data } => {
let body = output::read_json_input(data.as_deref())?;
let resp = c.index_doc(&index, &id, &body).await?;
output::render(&resp, fmt)
}
DocAction::Update { index, id, data } => {
let body = output::read_json_input(data.as_deref())?;
let resp = c.update_doc(&index, &id, &body).await?;
output::render(&resp, fmt)
}
DocAction::Delete { index, id } => {
let resp = c.delete_doc(&index, &id).await?;
output::render(&resp, fmt)
}
DocAction::Exists { index, id } => {
let exists = c.head_doc(&index, &id).await?;
println!("{exists}");
Ok(())
}
},
// -----------------------------------------------------------------
// Search query
// -----------------------------------------------------------------
SearchCommand::Query { index, query, data } => {
let body = if let Some(d) = data.as_deref() {
output::read_json_input(Some(d))?
} else if let Some(ref q) = query {
json!({ "query": { "query_string": { "query": q } } })
} else {
json!({ "query": { "match_all": {} } })
};
let resp = c.search(&index, &body).await?;
output::render(&resp, fmt)
}
// -----------------------------------------------------------------
// Count
// -----------------------------------------------------------------
SearchCommand::Count { index, data } => {
let body = if let Some(d) = data.as_deref() {
output::read_json_input(Some(d))?
} else {
json!({ "query": { "match_all": {} } })
};
let resp = c.count(&index, &body).await?;
output::render(&resp, fmt)
}
// -----------------------------------------------------------------
// Index management
// -----------------------------------------------------------------
SearchCommand::Index { action } => dispatch_index(&c, action, fmt).await,
// -----------------------------------------------------------------
// Cluster
// -----------------------------------------------------------------
SearchCommand::Cluster { action } => match action {
ClusterAction::Health => {
let resp = c.cluster_health().await?;
output::render(&resp, fmt)
}
ClusterAction::State => {
let resp = c.cluster_state().await?;
output::render(&resp, fmt)
}
ClusterAction::Stats => {
let resp = c.cluster_stats().await?;
output::render(&resp, fmt)
}
},
// -----------------------------------------------------------------
// Nodes
// -----------------------------------------------------------------
SearchCommand::Node { action } => match action {
NodeAction::List => {
let rows = c.cat_nodes().await?;
output::render_list(
&rows,
&["NAME", "IP", "HEAP%", "RAM%", "CPU", "ROLE", "MASTER"],
|n| {
vec![
n.name.clone().unwrap_or_default(),
n.ip.clone().unwrap_or_default(),
n.heap_percent.clone().unwrap_or_default(),
n.ram_percent.clone().unwrap_or_default(),
n.cpu.clone().unwrap_or_default(),
n.node_role.clone().unwrap_or_default(),
n.master.clone().unwrap_or_default(),
]
},
fmt,
)
}
NodeAction::Stats => {
let resp = c.nodes_stats().await?;
output::render(&resp, fmt)
}
NodeAction::Info => {
let resp = c.nodes_info().await?;
output::render(&resp, fmt)
}
},
// -----------------------------------------------------------------
// Ingest
// -----------------------------------------------------------------
SearchCommand::Ingest { action } => match action {
IngestAction::Pipeline { action } => match action {
PipelineAction::List => {
let resp = c.get_all_pipelines().await?;
output::render(&resp, fmt)
}
PipelineAction::Create { id, data } => {
let body = output::read_json_input(data.as_deref())?;
let resp = c.create_pipeline(&id, &body).await?;
output::render(&resp, fmt)
}
PipelineAction::Get { id } => {
let resp = c.get_pipeline(&id).await?;
output::render(&resp, fmt)
}
PipelineAction::Delete { id } => {
let resp = c.delete_pipeline(&id).await?;
output::render(&resp, fmt)
}
},
},
// -----------------------------------------------------------------
// Snapshots
// -----------------------------------------------------------------
SearchCommand::Snapshot { action } => dispatch_snapshot(&c, action, fmt).await,
}
}
// ---------------------------------------------------------------------------
// Index sub-dispatch
// ---------------------------------------------------------------------------
async fn dispatch_index(
c: &super::OpenSearchClient,
action: IndexAction,
fmt: OutputFormat,
) -> Result<()> {
match action {
IndexAction::List => {
let rows = c.cat_indices().await?;
output::render_list(
&rows,
&["HEALTH", "STATUS", "INDEX", "PRI", "REP", "DOCS", "SIZE"],
|i| {
vec![
i.health.clone().unwrap_or_default(),
i.status.clone().unwrap_or_default(),
i.index.clone().unwrap_or_default(),
i.pri.clone().unwrap_or_default(),
i.rep.clone().unwrap_or_default(),
i.docs_count.clone().unwrap_or_default(),
i.store_size.clone().unwrap_or_default(),
]
},
fmt,
)
}
IndexAction::Create { index, data } => {
let body = data
.as_deref()
.map(|d| output::read_json_input(Some(d)))
.transpose()?
.unwrap_or_else(|| json!({}));
let resp = c.create_index(&index, &body).await?;
output::render(&resp, fmt)
}
IndexAction::Get { index } => {
let resp = c.get_index(&index).await?;
output::render(&resp, fmt)
}
IndexAction::Delete { index } => {
let resp = c.delete_index(&index).await?;
output::render(&resp, fmt)
}
IndexAction::Exists { index } => {
let exists = c.index_exists(&index).await?;
println!("{exists}");
Ok(())
}
IndexAction::Open { index } => {
let resp = c.open_index(&index).await?;
output::render(&resp, fmt)
}
IndexAction::Close { index } => {
let resp = c.close_index(&index).await?;
output::render(&resp, fmt)
}
IndexAction::Mapping { action } => match action {
MappingAction::Get { index } => {
let resp = c.get_mapping(&index).await?;
output::render(&resp, fmt)
}
MappingAction::Update { index, data } => {
let body = output::read_json_input(data.as_deref())?;
let resp = c.put_mapping(&index, &body).await?;
output::render(&resp, fmt)
}
},
IndexAction::Settings { action } => match action {
SettingsAction::Get { index } => {
let resp = c.get_settings(&index).await?;
output::render(&resp, fmt)
}
SettingsAction::Update { index, data } => {
let body = output::read_json_input(data.as_deref())?;
let resp = c.update_settings(&index, &body).await?;
output::render(&resp, fmt)
}
},
}
}
// ---------------------------------------------------------------------------
// Snapshot sub-dispatch
// ---------------------------------------------------------------------------
async fn dispatch_snapshot(
c: &super::OpenSearchClient,
action: SnapshotAction,
fmt: OutputFormat,
) -> Result<()> {
match action {
SnapshotAction::List { repo } => {
let resp = c.list_snapshots(&repo).await?;
output::render(&resp, fmt)
}
SnapshotAction::Create { repo, name, data } => {
let body = data
.as_deref()
.map(|d| output::read_json_input(Some(d)))
.transpose()?
.unwrap_or_else(|| json!({}));
let resp = c.create_snapshot(&repo, &name, &body).await?;
output::render(&resp, fmt)
}
SnapshotAction::Delete { repo, name } => {
let resp = c.delete_snapshot(&repo, &name).await?;
output::render(&resp, fmt)
}
SnapshotAction::Restore { repo, name, data } => {
let body = data
.as_deref()
.map(|d| output::read_json_input(Some(d)))
.transpose()?
.unwrap_or_else(|| json!({}));
let resp = c.restore_snapshot(&repo, &name, &body).await?;
output::render(&resp, fmt)
}
SnapshotAction::Repo { action } => match action {
RepoAction::Create { name, data } => {
let body = output::read_json_input(data.as_deref())?;
let resp = c.create_snapshot_repo(&name, &body).await?;
output::render(&resp, fmt)
}
RepoAction::Delete { name } => {
let resp = c.delete_snapshot_repo(&name).await?;
output::render(&resp, fmt)
}
},
}
}