diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index 2ce4fe52..b8ff88ab 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -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 diff --git a/src/garage/cli/remote/mod.rs b/src/garage/cli/remote/mod.rs index f2516427..af79157c 100644 --- a/src/garage/cli/remote/mod.rs +++ b/src/garage/cli/remote/mod.rs @@ -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 { diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index d4446a17..9a6d912c 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -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, + }, } // ------------------------- diff --git a/src/garage/tests/common/garage.rs b/src/garage/tests/common/garage.rs index 8d71504f..3d4efbc2 100644 --- a/src/garage/tests/common/garage.rs +++ b/src/garage/tests/common/garage.rs @@ -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 }