feat: CLI modules for all 25+ service clients
One cli.rs per SDK module, gated behind #[cfg(feature = "cli")]: - auth (identity + hydra): identity, session, recovery, schema, courier, health, client, jwk, issuer, token, SSO passthrough - vcs (gitea): repo, issue, pr, branch, org, user, file, notification - chat (matrix): room, message, state, profile, device, user, sync - search (opensearch): doc, query, count, index, cluster, node, ingest pipeline, snapshot - storage (s3): bucket, object - media (livekit): room, participant, egress, token - mon (prometheus, loki, grafana): queries, dashboards, datasources, folders, annotations, alerts, org - vault (openbao): status, init, unseal, kv, policy, auth, secrets - la suite (people, docs, meet, drive, mail, cal, find) All dispatch functions take (cmd, &SunbeamClient, OutputFormat).
This commit is contained in:
268
sunbeam-sdk/src/storage/cli.rs
Normal file
268
sunbeam-sdk/src/storage/cli.rs
Normal file
@@ -0,0 +1,268 @@
|
||||
use clap::Subcommand;
|
||||
use std::io::Write;
|
||||
|
||||
use crate::client::SunbeamClient;
|
||||
use crate::error::Result;
|
||||
use crate::output::{self, OutputFormat};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Top-level StorageCommand
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum StorageCommand {
|
||||
/// Bucket operations.
|
||||
Bucket {
|
||||
#[command(subcommand)]
|
||||
action: BucketAction,
|
||||
},
|
||||
/// Object operations.
|
||||
Object {
|
||||
#[command(subcommand)]
|
||||
action: ObjectAction,
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bucket sub-commands
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum BucketAction {
|
||||
/// List all buckets.
|
||||
List,
|
||||
/// Create a bucket.
|
||||
Create {
|
||||
/// Bucket name.
|
||||
#[arg(short, long)]
|
||||
bucket: String,
|
||||
},
|
||||
/// Delete a bucket.
|
||||
Delete {
|
||||
/// Bucket name.
|
||||
#[arg(short, long)]
|
||||
bucket: String,
|
||||
},
|
||||
/// Check if a bucket exists.
|
||||
Exists {
|
||||
/// Bucket name.
|
||||
#[arg(short, long)]
|
||||
bucket: String,
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Object sub-commands
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum ObjectAction {
|
||||
/// List objects in a bucket.
|
||||
List {
|
||||
/// Bucket name.
|
||||
#[arg(short, long)]
|
||||
bucket: String,
|
||||
/// Filter by key prefix.
|
||||
#[arg(long)]
|
||||
prefix: Option<String>,
|
||||
/// Maximum number of keys to return.
|
||||
#[arg(long)]
|
||||
max_keys: Option<u32>,
|
||||
},
|
||||
/// Download an object.
|
||||
Get {
|
||||
/// Bucket name.
|
||||
#[arg(short, long)]
|
||||
bucket: String,
|
||||
/// Object key.
|
||||
#[arg(short, long)]
|
||||
key: String,
|
||||
/// Write to file instead of stdout.
|
||||
#[arg(long)]
|
||||
output_file: Option<String>,
|
||||
},
|
||||
/// Upload an object.
|
||||
Put {
|
||||
/// Bucket name.
|
||||
#[arg(short, long)]
|
||||
bucket: String,
|
||||
/// Object key.
|
||||
#[arg(short, long)]
|
||||
key: String,
|
||||
/// Content-Type header.
|
||||
#[arg(long, default_value = "application/octet-stream")]
|
||||
content_type: String,
|
||||
/// Path to the file to upload.
|
||||
#[arg(short, long)]
|
||||
file: String,
|
||||
},
|
||||
/// Delete an object.
|
||||
Delete {
|
||||
/// Bucket name.
|
||||
#[arg(short, long)]
|
||||
bucket: String,
|
||||
/// Object key.
|
||||
#[arg(short, long)]
|
||||
key: String,
|
||||
},
|
||||
/// Check if an object exists.
|
||||
Exists {
|
||||
/// Bucket name.
|
||||
#[arg(short, long)]
|
||||
bucket: String,
|
||||
/// Object key.
|
||||
#[arg(short, long)]
|
||||
key: String,
|
||||
},
|
||||
/// Copy an object.
|
||||
Copy {
|
||||
/// Destination bucket name.
|
||||
#[arg(short, long)]
|
||||
bucket: String,
|
||||
/// Destination object key.
|
||||
#[arg(short, long)]
|
||||
key: String,
|
||||
/// Source in the form `/source-bucket/source-key`.
|
||||
#[arg(short, long)]
|
||||
source: String,
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn dispatch(
|
||||
cmd: StorageCommand,
|
||||
client: &SunbeamClient,
|
||||
output: OutputFormat,
|
||||
) -> Result<()> {
|
||||
match cmd {
|
||||
StorageCommand::Bucket { action } => dispatch_bucket(action, client, output).await,
|
||||
StorageCommand::Object { action } => dispatch_object(action, client, output).await,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bucket dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn dispatch_bucket(
|
||||
action: BucketAction,
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let s3 = client.s3();
|
||||
match action {
|
||||
BucketAction::List => {
|
||||
let resp = s3.list_buckets().await?;
|
||||
output::render_list(
|
||||
&resp.buckets,
|
||||
&["NAME", "CREATED"],
|
||||
|b| {
|
||||
vec![
|
||||
b.name.clone(),
|
||||
b.creation_date.clone().unwrap_or_default(),
|
||||
]
|
||||
},
|
||||
fmt,
|
||||
)
|
||||
}
|
||||
BucketAction::Create { bucket } => {
|
||||
s3.create_bucket(&bucket).await?;
|
||||
output::ok(&format!("Created bucket {bucket}"));
|
||||
Ok(())
|
||||
}
|
||||
BucketAction::Delete { bucket } => {
|
||||
s3.delete_bucket(&bucket).await?;
|
||||
output::ok(&format!("Deleted bucket {bucket}"));
|
||||
Ok(())
|
||||
}
|
||||
BucketAction::Exists { bucket } => {
|
||||
let exists = s3.head_bucket(&bucket).await?;
|
||||
output::render(&serde_json::json!({ "exists": exists }), fmt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Object dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn dispatch_object(
|
||||
action: ObjectAction,
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let s3 = client.s3();
|
||||
match action {
|
||||
ObjectAction::List {
|
||||
bucket,
|
||||
prefix,
|
||||
max_keys,
|
||||
} => {
|
||||
let resp = s3
|
||||
.list_objects_v2(&bucket, prefix.as_deref(), max_keys)
|
||||
.await?;
|
||||
output::render_list(
|
||||
&resp.contents,
|
||||
&["KEY", "SIZE", "LAST_MODIFIED", "ETAG"],
|
||||
|o| {
|
||||
vec![
|
||||
o.key.clone(),
|
||||
o.size.map_or("-".into(), |s| s.to_string()),
|
||||
o.last_modified.clone().unwrap_or_default(),
|
||||
o.etag.clone().unwrap_or_default(),
|
||||
]
|
||||
},
|
||||
fmt,
|
||||
)
|
||||
}
|
||||
ObjectAction::Get {
|
||||
bucket,
|
||||
key,
|
||||
output_file,
|
||||
} => {
|
||||
let data = s3.get_object(&bucket, &key).await?;
|
||||
match output_file {
|
||||
Some(path) => {
|
||||
std::fs::write(&path, &data)?;
|
||||
output::ok(&format!("Written {} bytes to {path}", data.len()));
|
||||
}
|
||||
None => {
|
||||
std::io::stdout().write_all(&data)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
ObjectAction::Put {
|
||||
bucket,
|
||||
key,
|
||||
content_type,
|
||||
file,
|
||||
} => {
|
||||
let data = bytes::Bytes::from(std::fs::read(&file)?);
|
||||
s3.put_object(&bucket, &key, &content_type, data).await?;
|
||||
output::ok(&format!("Uploaded {file} to {bucket}/{key}"));
|
||||
Ok(())
|
||||
}
|
||||
ObjectAction::Delete { bucket, key } => {
|
||||
s3.delete_object(&bucket, &key).await?;
|
||||
output::ok(&format!("Deleted {bucket}/{key}"));
|
||||
Ok(())
|
||||
}
|
||||
ObjectAction::Exists { bucket, key } => {
|
||||
let exists = s3.head_object(&bucket, &key).await?;
|
||||
output::render(&serde_json::json!({ "exists": exists }), fmt)
|
||||
}
|
||||
ObjectAction::Copy {
|
||||
bucket,
|
||||
key,
|
||||
source,
|
||||
} => {
|
||||
s3.copy_object(&bucket, &key, &source).await?;
|
||||
output::ok(&format!("Copied {source} to {bucket}/{key}"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user