cli: add garage json-api command and fix cargo tests

This commit is contained in:
Alex Auvolat 2025-03-12 15:47:13 +01:00
parent e6862c5d3d
commit f7d9c2b383
4 changed files with 64 additions and 16 deletions

View file

@ -50,6 +50,7 @@ sodiumoxide.workspace = true
structopt.workspace = true
git-version.workspace = true
utoipa.workspace = true
serde_json.workspace = true
futures.workspace = true
tokio.workspace = true

View file

@ -43,6 +43,7 @@ impl Cli {
Command::Meta(mo) => self.cmd_meta(mo).await,
Command::Stats(so) => self.cmd_stats(so).await,
Command::Repair(ro) => self.cmd_repair(ro).await,
Command::JsonApi { endpoint, payload } => self.cmd_json_api(endpoint, payload).await,
_ => unreachable!(),
}
@ -105,6 +106,49 @@ impl Cli {
}
Ok(resp.success.into_iter().next().unwrap().1)
}
pub async fn cmd_json_api(&self, endpoint: String, payload: String) -> Result<(), Error> {
let payload: serde_json::Value = if payload == "-" {
serde_json::from_reader(&std::io::stdin())?
} else {
serde_json::from_str(&payload)?
};
let request: AdminApiRequest = serde_json::from_value(serde_json::json!({
endpoint.clone(): payload,
}))?;
let resp = match self
.proxy_rpc_endpoint
.call(&self.rpc_host, ProxyRpc::Proxy(request), PRIO_NORMAL)
.await??
{
ProxyRpcResponse::ProxyApiOkResponse(resp) => resp,
ProxyRpcResponse::ApiErrorResponse {
http_code,
error_code,
message,
} => {
return Err(Error::Message(format!(
"{} ({}): {}",
error_code, http_code, message
)))
}
m => return Err(Error::unexpected_rpc_message(m)),
};
if let serde_json::Value::Object(map) = serde_json::to_value(&resp)? {
if let Some(inner) = map.get(&endpoint) {
serde_json::to_writer_pretty(std::io::stdout(), &inner)?;
return Ok(());
}
}
Err(Error::Message(format!(
"Invalid response: {}",
serde_json::to_string(&resp)?
)))
}
}
pub fn table_list_abbr<T: IntoIterator<Item = S>, S: AsRef<str>>(values: T) -> String {

View file

@ -66,6 +66,17 @@ pub enum Command {
/// Output openapi JSON schema for admin api
#[structopt(name = "admin-api-schema", version = garage_version(), setting(structopt::clap::AppSettings::Hidden))]
AdminApiSchema,
/// Directly invoke the admin API using a JSON payload.
/// The result is printed to `stdout` in JSON format.
#[structopt(name = "json-api", version = garage_version())]
JsonApi {
/// The admin API endpoint to invoke, e.g. GetClusterStatus
endpoint: String,
/// The JSON payload, or `-` to read from `stdin`
#[structopt(default_value = "null")]
payload: String,
},
}
// -------------------------

View file

@ -3,6 +3,8 @@ use std::path::{Path, PathBuf};
use std::process;
use std::sync::Once;
use serde_json::json;
use super::ext::*;
// https://xkcd.com/221/
@ -193,27 +195,17 @@ api_bind_addr = "127.0.0.1:{admin_port}"
let mut key = Key::default();
let mut cmd = self.command();
let base = cmd.args(["key", "create"]);
let base = cmd.args(["json-api", "CreateKey"]);
let with_name = match maybe_name {
Some(name) => base.args([name]),
None => base,
Some(name) => base.args([serde_json::to_string(&json!({"name": name})).unwrap()]),
None => base.args(["{}"]),
};
let output = with_name.expect_success_output("Could not create key");
let stdout = String::from_utf8(output.stdout).unwrap();
let stdout: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
for line in stdout.lines() {
if let Some(key_id) = line.strip_prefix("Key ID: ") {
key.id = key_id.to_owned();
continue;
}
if let Some(key_secret) = line.strip_prefix("Secret key: ") {
key.secret = key_secret.to_owned();
continue;
}
}
assert!(!key.id.is_empty(), "Invalid key: Key ID is empty");
assert!(!key.secret.is_empty(), "Invalid key: Key secret is empty");
key.id = stdout["accessKeyId"].as_str().unwrap().to_string();
key.secret = stdout["secretAccessKey"].as_str().unwrap().to_string();
key
}