refactor coordinator to use shared lib models and client
This commit is contained in:
@@ -25,6 +25,10 @@ hero-job = { path = "../../models/job" }
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
jsonrpsee = { workspace = true, features = ["http-client", "macros"] }
|
||||
tokio.workspace = true
|
||||
async-trait.workspace = true
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
base64 = "0.22"
|
||||
tracing.workspace = true
|
||||
# hero-job-client removed - now part of supervisor
|
||||
env_logger.workspace = true
|
||||
http.workspace = true
|
||||
|
||||
@@ -2,6 +2,13 @@ use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use serde_json;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use async_trait::async_trait;
|
||||
|
||||
// Transport implementations
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod transports;
|
||||
|
||||
// Import types from the main supervisor crate
|
||||
|
||||
|
||||
@@ -32,19 +39,69 @@ use jsonrpsee::{
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use http::{HeaderMap, HeaderName, HeaderValue};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Client for communicating with Hero Supervisor OpenRPC server
|
||||
/// Requires authentication secret for all operations
|
||||
/// Transport abstraction for supervisor communication
|
||||
/// Allows different transport layers (HTTP, Mycelium, etc.)
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[async_trait]
|
||||
pub trait SupervisorTransport: Send + Sync {
|
||||
/// Send a JSON-RPC request and await the response
|
||||
async fn call(
|
||||
&self,
|
||||
method: &str,
|
||||
params: serde_json::Value,
|
||||
) -> Result<serde_json::Value, ClientError>;
|
||||
}
|
||||
|
||||
/// HTTP transport implementation using jsonrpsee
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Clone)]
|
||||
pub struct SupervisorClient {
|
||||
pub struct HttpTransport {
|
||||
client: HttpClient,
|
||||
server_url: String,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[async_trait]
|
||||
impl SupervisorTransport for HttpTransport {
|
||||
async fn call(
|
||||
&self,
|
||||
method: &str,
|
||||
params: serde_json::Value,
|
||||
) -> Result<serde_json::Value, ClientError> {
|
||||
// params is already an array from the caller
|
||||
// jsonrpsee expects params as an array, so pass it directly
|
||||
let result: serde_json::Value = if params.is_array() {
|
||||
// Use the array directly with rpc_params
|
||||
let arr = params.as_array().unwrap();
|
||||
match arr.len() {
|
||||
0 => self.client.request(method, rpc_params![]).await?,
|
||||
1 => self.client.request(method, rpc_params![&arr[0]]).await?,
|
||||
_ => {
|
||||
// For multiple params, we need to pass them as a slice
|
||||
self.client.request(method, rpc_params![arr]).await?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single param not in array
|
||||
self.client.request(method, rpc_params![¶ms]).await?
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Client for communicating with Hero Supervisor OpenRPC server
|
||||
/// Generic over transport layer (HTTP, Mycelium, etc.)
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Clone)]
|
||||
pub struct SupervisorClient<T: SupervisorTransport = HttpTransport> {
|
||||
transport: T,
|
||||
secret: String,
|
||||
}
|
||||
|
||||
/// Legacy type alias for backward compatibility
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub type HttpSupervisorClient = SupervisorClient<HttpTransport>;
|
||||
|
||||
/// Error types for client operations
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Error, Debug)]
|
||||
@@ -258,8 +315,8 @@ impl SupervisorClientBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the SupervisorClient
|
||||
pub fn build(self) -> ClientResult<SupervisorClient> {
|
||||
/// Build the SupervisorClient with HTTP transport
|
||||
pub fn build(self) -> ClientResult<SupervisorClient<HttpTransport>> {
|
||||
let server_url = self.url
|
||||
.ok_or_else(|| ClientError::Http("URL is required".to_string()))?;
|
||||
let secret = self.secret
|
||||
@@ -280,9 +337,10 @@ impl SupervisorClientBuilder {
|
||||
.build(&server_url)
|
||||
.map_err(|e| ClientError::Http(e.to_string()))?;
|
||||
|
||||
let transport = HttpTransport { client };
|
||||
|
||||
Ok(SupervisorClient {
|
||||
client,
|
||||
server_url,
|
||||
transport,
|
||||
secret,
|
||||
})
|
||||
}
|
||||
@@ -296,25 +354,24 @@ impl Default for SupervisorClientBuilder {
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl SupervisorClient {
|
||||
/// Create a builder for SupervisorClient
|
||||
impl SupervisorClient<HttpTransport> {
|
||||
/// Create a builder for HTTP-based SupervisorClient
|
||||
pub fn builder() -> SupervisorClientBuilder {
|
||||
SupervisorClientBuilder::new()
|
||||
}
|
||||
|
||||
/// Get the server URL
|
||||
pub fn server_url(&self) -> &str {
|
||||
&self.server_url
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl<T: SupervisorTransport> SupervisorClient<T> {
|
||||
/// Create a new client with a custom transport
|
||||
pub fn new(transport: T, secret: String) -> Self {
|
||||
Self { transport, secret }
|
||||
}
|
||||
|
||||
/// Test connection using OpenRPC discovery method
|
||||
/// This calls the standard `rpc.discover` method that should be available on any OpenRPC server
|
||||
pub async fn discover(&self) -> ClientResult<serde_json::Value> {
|
||||
let result: serde_json::Value = self
|
||||
.client
|
||||
.request("rpc.discover", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(result)
|
||||
self.transport.call("rpc.discover", serde_json::json!([])).await
|
||||
}
|
||||
|
||||
/// Register a new runner to the supervisor
|
||||
@@ -324,11 +381,8 @@ impl SupervisorClient {
|
||||
&self,
|
||||
name: &str,
|
||||
) -> ClientResult<()> {
|
||||
let _: () = self
|
||||
.client
|
||||
.request("runner.create", rpc_params![name])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(())
|
||||
let result = self.transport.call("runner.create", serde_json::json!([name])).await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Create a new job without queuing it to a runner
|
||||
@@ -337,20 +391,14 @@ impl SupervisorClient {
|
||||
&self,
|
||||
job: Job,
|
||||
) -> ClientResult<String> {
|
||||
let job_id: String = self
|
||||
.client
|
||||
.request("job.create", rpc_params![job])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(job_id)
|
||||
let result = self.transport.call("job.create", serde_json::json!([job])).await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// List all jobs
|
||||
pub async fn job_list(&self) -> ClientResult<Vec<Job>> {
|
||||
let jobs: Vec<Job> = self
|
||||
.client
|
||||
.request("job.list", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(jobs)
|
||||
let result = self.transport.call("job.list", serde_json::json!([])).await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Run a job on the appropriate runner and wait for the result (blocking)
|
||||
@@ -369,11 +417,8 @@ impl SupervisorClient {
|
||||
params["timeout"] = serde_json::json!(t);
|
||||
}
|
||||
|
||||
let result: JobRunResponse = self
|
||||
.client
|
||||
.request("job.run", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(result)
|
||||
let result = self.transport.call("job.run", serde_json::json!([params])).await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Start a job without waiting for the result (non-blocking)
|
||||
@@ -387,58 +432,40 @@ impl SupervisorClient {
|
||||
"job": job
|
||||
});
|
||||
|
||||
let result: JobStartResponse = self
|
||||
.client
|
||||
.request("job.start", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(result)
|
||||
let result = self.transport.call("job.start", serde_json::json!([params])).await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Get the current status of a job
|
||||
pub async fn job_status(&self, job_id: &str) -> ClientResult<JobStatus> {
|
||||
let status: JobStatus = self
|
||||
.client
|
||||
.request("job.status", rpc_params![job_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(status)
|
||||
let result = self.transport.call("job.status", serde_json::json!([job_id])).await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Get the result of a completed job (blocks until result is available)
|
||||
pub async fn job_result(&self, job_id: &str) -> ClientResult<JobResult> {
|
||||
let result: JobResult = self
|
||||
.client
|
||||
.request("job.result", rpc_params![job_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(result)
|
||||
let result = self.transport.call("job.result", serde_json::json!([job_id])).await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Remove a runner from the supervisor
|
||||
/// Authentication via Authorization header (set during client creation)
|
||||
pub async fn runner_remove(&self, runner_id: &str) -> ClientResult<()> {
|
||||
let _: () = self
|
||||
.client
|
||||
.request("runner.remove", rpc_params![runner_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(())
|
||||
let result = self.transport.call("runner.remove", serde_json::json!([runner_id])).await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// List all runner IDs
|
||||
pub async fn runner_list(&self) -> ClientResult<Vec<String>> {
|
||||
let runners: Vec<String> = self
|
||||
.client
|
||||
.request("runner.list", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(runners)
|
||||
let result = self.transport.call("runner.list", serde_json::json!([])).await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Start a specific runner
|
||||
/// Authentication via Authorization header (set during client creation)
|
||||
pub async fn start_runner(&self, actor_id: &str) -> ClientResult<()> {
|
||||
let _: () = self
|
||||
.client
|
||||
.request("runner.start", rpc_params![actor_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(())
|
||||
let result = self.transport.call("runner.start", serde_json::json!([actor_id])).await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Add a runner to the supervisor
|
||||
@@ -447,21 +474,21 @@ impl SupervisorClient {
|
||||
let params = serde_json::json!({
|
||||
"config": config
|
||||
});
|
||||
let _: () = self
|
||||
.client
|
||||
.request("runner.add", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
let result = self
|
||||
.transport
|
||||
.call("runner.add", serde_json::json!([params]))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get status of a specific runner
|
||||
/// Authentication via Authorization header (set during client creation)
|
||||
pub async fn get_runner_status(&self, actor_id: &str) -> ClientResult<RunnerStatus> {
|
||||
let status: RunnerStatus = self
|
||||
.client
|
||||
.request("runner.status", rpc_params![actor_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(status)
|
||||
let result = self
|
||||
.transport
|
||||
.call("runner.status", serde_json::json!([actor_id]))
|
||||
.await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Get logs for a specific runner
|
||||
@@ -471,11 +498,11 @@ impl SupervisorClient {
|
||||
lines: Option<usize>,
|
||||
follow: bool,
|
||||
) -> ClientResult<Vec<LogInfo>> {
|
||||
let logs: Vec<LogInfo> = self
|
||||
.client
|
||||
.request("get_runner_logs", rpc_params![actor_id, lines, follow])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(logs)
|
||||
let result = self
|
||||
.transport
|
||||
.call("get_runner_logs", serde_json::json!([actor_id, lines, follow]))
|
||||
.await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Queue a job to a specific runner
|
||||
@@ -485,10 +512,10 @@ impl SupervisorClient {
|
||||
"job": job
|
||||
});
|
||||
|
||||
let _: () = self
|
||||
.client
|
||||
.request("queue_job_to_runner", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
let result = self
|
||||
.transport
|
||||
.call("queue_job_to_runner", serde_json::json!([params]))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -500,11 +527,11 @@ impl SupervisorClient {
|
||||
"timeout_secs": timeout_secs
|
||||
});
|
||||
|
||||
let result: Option<String> = self
|
||||
.client
|
||||
.request("queue_and_wait", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(result)
|
||||
let result = self
|
||||
.transport
|
||||
.call("queue_and_wait", serde_json::json!([params]))
|
||||
.await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Run a job on a specific runner
|
||||
@@ -513,56 +540,56 @@ impl SupervisorClient {
|
||||
"job": job
|
||||
});
|
||||
|
||||
let result: JobResult = self
|
||||
.client
|
||||
.request("job.run", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(result)
|
||||
let result = self
|
||||
.transport
|
||||
.call("job.run", serde_json::json!([params]))
|
||||
.await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Get job result by job ID
|
||||
pub async fn get_job_result(&self, job_id: &str) -> ClientResult<Option<String>> {
|
||||
let result: Option<String> = self
|
||||
.client
|
||||
.request("get_job_result", rpc_params![job_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(result)
|
||||
let result = self
|
||||
.transport
|
||||
.call("get_job_result", serde_json::json!([job_id]))
|
||||
.await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Get status of all runners
|
||||
pub async fn get_all_runner_status(&self) -> ClientResult<Vec<(String, RunnerStatus)>> {
|
||||
let statuses: Vec<(String, RunnerStatus)> = self
|
||||
.client
|
||||
.request("get_all_runner_status", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(statuses)
|
||||
let result = self
|
||||
.transport
|
||||
.call("get_all_runner_status", serde_json::json!([]))
|
||||
.await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Start all runners
|
||||
pub async fn start_all(&self) -> ClientResult<Vec<(String, bool)>> {
|
||||
let results: Vec<(String, bool)> = self
|
||||
.client
|
||||
.request("start_all", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(results)
|
||||
let result = self
|
||||
.transport
|
||||
.call("start_all", serde_json::json!([]))
|
||||
.await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Stop all runners
|
||||
pub async fn stop_all(&self, force: bool) -> ClientResult<Vec<(String, bool)>> {
|
||||
let results: Vec<(String, bool)> = self
|
||||
.client
|
||||
.request("stop_all", rpc_params![force])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(results)
|
||||
let result = self
|
||||
.transport
|
||||
.call("stop_all", serde_json::json!([force]))
|
||||
.await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Get status of all runners (alternative method)
|
||||
pub async fn get_all_status(&self) -> ClientResult<Vec<(String, RunnerStatus)>> {
|
||||
let statuses: Vec<(String, RunnerStatus)> = self
|
||||
.client
|
||||
.request("get_all_status", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(statuses)
|
||||
let result = self
|
||||
.transport
|
||||
.call("get_all_status", serde_json::json!([]))
|
||||
.await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Add a secret to the supervisor
|
||||
@@ -576,10 +603,10 @@ impl SupervisorClient {
|
||||
"secret_value": secret_value
|
||||
});
|
||||
|
||||
let _: () = self
|
||||
.client
|
||||
.request("add_secret", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
let result = self
|
||||
.transport
|
||||
.call("add_secret", serde_json::json!([params]))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -594,10 +621,10 @@ impl SupervisorClient {
|
||||
"secret_value": secret_value
|
||||
});
|
||||
|
||||
let _: () = self
|
||||
.client
|
||||
.request("remove_secret", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
let result = self
|
||||
.transport
|
||||
.call("remove_secret", serde_json::json!([params]))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -605,91 +632,87 @@ impl SupervisorClient {
|
||||
pub async fn list_secrets(&self) -> ClientResult<SupervisorInfo> {
|
||||
let params = serde_json::json!({});
|
||||
|
||||
let info: SupervisorInfo = self
|
||||
.client
|
||||
.request("list_secrets", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(info)
|
||||
let result = self
|
||||
.transport
|
||||
.call("list_secrets", serde_json::json!([params]))
|
||||
.await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Stop a running job
|
||||
pub async fn job_stop(&self, job_id: &str) -> ClientResult<()> {
|
||||
let _: () = self.client
|
||||
.request("job.stop", rpc_params![job_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(())
|
||||
let result = self.transport.call("job.stop", serde_json::json!([job_id])).await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Delete a job from the system
|
||||
pub async fn job_delete(&self, job_id: &str) -> ClientResult<()> {
|
||||
let _: () = self.client
|
||||
.request("job.delete", rpc_params![job_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(())
|
||||
let result = self.transport.call("job.delete", serde_json::json!([job_id])).await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Get supervisor information including secret counts
|
||||
pub async fn get_supervisor_info(&self) -> ClientResult<SupervisorInfo> {
|
||||
let info: SupervisorInfo = self
|
||||
.client
|
||||
.request("supervisor.info", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(info)
|
||||
let result = self
|
||||
.transport
|
||||
.call("supervisor.info", serde_json::json!([]))
|
||||
.await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Get a job by ID
|
||||
pub async fn job_get(&self, job_id: &str) -> ClientResult<Job> {
|
||||
let job: Job = self
|
||||
.client
|
||||
.request("job.get", rpc_params![job_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(job)
|
||||
let result = self
|
||||
.transport
|
||||
.call("job.get", serde_json::json!([job_id]))
|
||||
.await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
// ========== Auth/API Key Methods ==========
|
||||
|
||||
/// Verify the current API key
|
||||
pub async fn auth_verify(&self) -> ClientResult<AuthVerifyResponse> {
|
||||
let response: AuthVerifyResponse = self
|
||||
.client
|
||||
.request("auth.verify", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(response)
|
||||
let result = self
|
||||
.transport
|
||||
.call("auth.verify", serde_json::json!([]))
|
||||
.await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Create a new API key (admin only)
|
||||
pub async fn key_create(&self, key: ApiKey) -> ClientResult<()> {
|
||||
let _: () = self
|
||||
.client
|
||||
.request("key.create", rpc_params![key])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
let result = self
|
||||
.transport
|
||||
.call("key.create", serde_json::json!([key]))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate a new API key with auto-generated key value (admin only)
|
||||
pub async fn key_generate(&self, params: GenerateApiKeyParams) -> ClientResult<ApiKey> {
|
||||
let api_key: ApiKey = self
|
||||
.client
|
||||
.request("key.generate", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(api_key)
|
||||
let result = self
|
||||
.transport
|
||||
.call("key.generate", serde_json::json!([params]))
|
||||
.await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
|
||||
/// Remove an API key (admin only)
|
||||
pub async fn key_delete(&self, key_id: String) -> ClientResult<()> {
|
||||
let _: () = self
|
||||
.client
|
||||
.request("key.delete", rpc_params![key_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
let result = self
|
||||
.transport
|
||||
.call("key.delete", serde_json::json!([key_id]))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all API keys (admin only)
|
||||
pub async fn key_list(&self) -> ClientResult<Vec<ApiKey>> {
|
||||
let keys: Vec<ApiKey> = self
|
||||
.client
|
||||
.request("key.list", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(keys)
|
||||
let result = self
|
||||
.transport
|
||||
.call("key.list", serde_json::json!([]))
|
||||
.await?;
|
||||
serde_json::from_value(result).map_err(ClientError::Serialization)
|
||||
}
|
||||
}
|
||||
13
lib/clients/supervisor/src/transports/mod.rs
Normal file
13
lib/clients/supervisor/src/transports/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
/// Mycelium transport for supervisor communication
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod mycelium;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use mycelium::{
|
||||
Destination,
|
||||
MyceliumClient,
|
||||
MyceliumClientError,
|
||||
MyceliumTransport,
|
||||
SupervisorHub,
|
||||
TransportStatus,
|
||||
};
|
||||
366
lib/clients/supervisor/src/transports/mycelium.rs
Normal file
366
lib/clients/supervisor/src/transports/mycelium.rs
Normal file
@@ -0,0 +1,366 @@
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use reqwest::Client as HttpClient;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::{Mutex, oneshot};
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::{SupervisorTransport, ClientError};
|
||||
|
||||
/// Destination for Mycelium messages
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Destination {
|
||||
Ip(IpAddr),
|
||||
/// 64-hex public key of the receiver node
|
||||
Pk(String),
|
||||
}
|
||||
|
||||
/// Transport status from Mycelium
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TransportStatus {
|
||||
Pending,
|
||||
Sent,
|
||||
Delivered,
|
||||
Failed,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
/// Lightweight client for Mycelium JSON-RPC (send + query status)
|
||||
#[derive(Clone)]
|
||||
pub struct MyceliumClient {
|
||||
base_url: String, // e.g. http://127.0.0.1:8990
|
||||
http: HttpClient,
|
||||
id_counter: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MyceliumClientError {
|
||||
#[error("HTTP error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
#[error("JSON error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error("Transport timed out waiting for a reply (408)")]
|
||||
TransportTimeout,
|
||||
#[error("JSON-RPC error: {0}")]
|
||||
RpcError(String),
|
||||
#[error("Invalid response: {0}")]
|
||||
InvalidResponse(String),
|
||||
}
|
||||
|
||||
impl From<MyceliumClientError> for ClientError {
|
||||
fn from(e: MyceliumClientError) -> Self {
|
||||
match e {
|
||||
MyceliumClientError::Http(err) => ClientError::Http(err.to_string()),
|
||||
MyceliumClientError::Json(err) => ClientError::Serialization(err),
|
||||
MyceliumClientError::TransportTimeout => ClientError::Server { message: "Transport timeout".to_string() },
|
||||
MyceliumClientError::RpcError(msg) => ClientError::Server { message: msg },
|
||||
MyceliumClientError::InvalidResponse(msg) => ClientError::Server { message: msg },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MyceliumClient {
|
||||
pub fn new(base_url: impl Into<String>) -> Result<Self, MyceliumClientError> {
|
||||
let url = base_url.into();
|
||||
let http = HttpClient::builder().build()?;
|
||||
Ok(Self {
|
||||
base_url: url,
|
||||
http,
|
||||
id_counter: Arc::new(AtomicU64::new(1)),
|
||||
})
|
||||
}
|
||||
|
||||
fn next_id(&self) -> u64 {
|
||||
self.id_counter.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
async fn jsonrpc(&self, method: &str, params: Value) -> Result<Value, MyceliumClientError> {
|
||||
let req = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": self.next_id(),
|
||||
"method": method,
|
||||
"params": [ params ]
|
||||
});
|
||||
|
||||
tracing::info!(%req, "jsonrpc");
|
||||
let resp = self.http.post(&self.base_url).json(&req).send().await?;
|
||||
let status = resp.status();
|
||||
let body: Value = resp.json().await?;
|
||||
if let Some(err) = body.get("error") {
|
||||
let code = err.get("code").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
let msg = err
|
||||
.get("message")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown error");
|
||||
if code == 408 {
|
||||
return Err(MyceliumClientError::TransportTimeout);
|
||||
}
|
||||
return Err(MyceliumClientError::RpcError(format!(
|
||||
"code={code} msg={msg}"
|
||||
)));
|
||||
}
|
||||
if !status.is_success() {
|
||||
return Err(MyceliumClientError::RpcError(format!(
|
||||
"HTTP {status}, body {body}"
|
||||
)));
|
||||
}
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Call messageStatus with an outbound message id (hex string)
|
||||
pub async fn message_status(
|
||||
&self,
|
||||
id_hex: &str,
|
||||
) -> Result<TransportStatus, MyceliumClientError> {
|
||||
let params = json!(id_hex);
|
||||
let body = self.jsonrpc("getMessageInfo", params).await?;
|
||||
let result = body.get("result").ok_or_else(|| {
|
||||
MyceliumClientError::InvalidResponse(format!("missing result in response: {body}"))
|
||||
})?;
|
||||
// Accept both { state: "..."} and bare "..."
|
||||
let status_str = if let Some(s) = result.get("state").and_then(|v| v.as_str()) {
|
||||
s.to_string()
|
||||
} else if let Some(s) = result.as_str() {
|
||||
s.to_string()
|
||||
} else {
|
||||
return Err(MyceliumClientError::InvalidResponse(format!(
|
||||
"expected string or object with state, got {result}"
|
||||
)));
|
||||
};
|
||||
|
||||
match status_str.as_str() {
|
||||
"pending" => Ok(TransportStatus::Pending),
|
||||
"sent" => Ok(TransportStatus::Sent),
|
||||
"delivered" => Ok(TransportStatus::Delivered),
|
||||
"failed" => Ok(TransportStatus::Failed),
|
||||
"timeout" => Ok(TransportStatus::Timeout),
|
||||
_ => Err(MyceliumClientError::InvalidResponse(format!(
|
||||
"unknown status: {status_str}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a message via Mycelium
|
||||
pub async fn push_message(
|
||||
&self,
|
||||
dst: Value,
|
||||
topic: &str,
|
||||
payload: &str,
|
||||
) -> Result<String, MyceliumClientError> {
|
||||
let params = json!({
|
||||
"dst": dst,
|
||||
"topic": BASE64_STANDARD.encode(topic.as_bytes()),
|
||||
"payload": payload,
|
||||
});
|
||||
|
||||
let body = self.jsonrpc("pushMessage", params).await?;
|
||||
let result = body.get("result").ok_or_else(|| {
|
||||
MyceliumClientError::InvalidResponse(format!("missing result in pushMessage response"))
|
||||
})?;
|
||||
|
||||
// Extract message ID
|
||||
result
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| {
|
||||
MyceliumClientError::InvalidResponse(format!("missing id in result: {result}"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Pop a message from a topic
|
||||
pub async fn pop_message(&self, topic: &str) -> Result<Option<Value>, MyceliumClientError> {
|
||||
let params = json!({
|
||||
"topic": BASE64_STANDARD.encode(topic.as_bytes()),
|
||||
});
|
||||
|
||||
let body = self.jsonrpc("popMessage", params).await?;
|
||||
let result = body.get("result").ok_or_else(|| {
|
||||
MyceliumClientError::InvalidResponse(format!("missing result in popMessage response"))
|
||||
})?;
|
||||
|
||||
if result.is_null() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(result.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hub that manages request/reply correlation for supervisor calls via Mycelium
|
||||
pub struct SupervisorHub {
|
||||
mycelium: Arc<MyceliumClient>,
|
||||
topic: String,
|
||||
id_counter: Arc<AtomicU64>,
|
||||
waiters: Arc<Mutex<HashMap<u64, oneshot::Sender<Value>>>>,
|
||||
}
|
||||
|
||||
impl SupervisorHub {
|
||||
pub fn new_with_client(mycelium: Arc<MyceliumClient>, topic: impl Into<String>) -> Arc<Self> {
|
||||
let hub = Arc::new(Self {
|
||||
mycelium,
|
||||
topic: topic.into(),
|
||||
id_counter: Arc::new(AtomicU64::new(1)),
|
||||
waiters: Arc::new(Mutex::new(HashMap::new())),
|
||||
});
|
||||
|
||||
// Spawn background listener
|
||||
let hub_clone = hub.clone();
|
||||
tokio::spawn(async move {
|
||||
hub_clone.listen_loop().await;
|
||||
});
|
||||
|
||||
hub
|
||||
}
|
||||
|
||||
pub fn next_id(&self) -> u64 {
|
||||
self.id_counter.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub async fn register_waiter(&self, id: u64) -> oneshot::Receiver<Value> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.waiters.lock().await.insert(id, tx);
|
||||
rx
|
||||
}
|
||||
|
||||
async fn listen_loop(&self) {
|
||||
loop {
|
||||
match self.mycelium.pop_message(&self.topic).await {
|
||||
Ok(Some(envelope)) => {
|
||||
if let Err(e) = self.handle_message(envelope).await {
|
||||
tracing::warn!("Failed to handle message: {}", e);
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// No message, wait a bit
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error popping message: {}", e);
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_message(&self, envelope: Value) -> Result<(), String> {
|
||||
// Decode payload
|
||||
let payload_b64 = envelope
|
||||
.get("payload")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| "missing payload".to_string())?;
|
||||
|
||||
let payload_bytes = BASE64_STANDARD
|
||||
.decode(payload_b64)
|
||||
.map_err(|e| format!("base64 decode error: {}", e))?;
|
||||
|
||||
let payload_str = String::from_utf8(payload_bytes)
|
||||
.map_err(|e| format!("utf8 decode error: {}", e))?;
|
||||
|
||||
let reply: Value = serde_json::from_str(&payload_str)
|
||||
.map_err(|e| format!("json parse error: {}", e))?;
|
||||
|
||||
// Extract ID
|
||||
let id = reply
|
||||
.get("id")
|
||||
.and_then(|v| v.as_u64())
|
||||
.ok_or_else(|| "missing or invalid id in reply".to_string())?;
|
||||
|
||||
// Notify waiter
|
||||
if let Some(tx) = self.waiters.lock().await.remove(&id) {
|
||||
let _ = tx.send(reply);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Mycelium transport implementation for SupervisorClient
|
||||
pub struct MyceliumTransport {
|
||||
hub: Arc<SupervisorHub>,
|
||||
destination: Destination,
|
||||
timeout_secs: u64,
|
||||
}
|
||||
|
||||
impl MyceliumTransport {
|
||||
pub fn new(hub: Arc<SupervisorHub>, destination: Destination) -> Self {
|
||||
Self {
|
||||
hub,
|
||||
destination,
|
||||
timeout_secs: 10,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
|
||||
self.timeout_secs = timeout_secs;
|
||||
self
|
||||
}
|
||||
|
||||
fn build_dst(&self) -> Value {
|
||||
match &self.destination {
|
||||
Destination::Ip(ip) => json!({ "ip": ip.to_string() }),
|
||||
Destination::Pk(pk) => json!({ "pk": pk }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SupervisorTransport for MyceliumTransport {
|
||||
async fn call(
|
||||
&self,
|
||||
method: &str,
|
||||
params: Value,
|
||||
) -> Result<Value, ClientError> {
|
||||
let inner_id = self.hub.next_id();
|
||||
|
||||
// Register waiter before sending
|
||||
let rx = self.hub.register_waiter(inner_id).await;
|
||||
|
||||
// Build JSON-RPC payload
|
||||
let inner = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": inner_id,
|
||||
"method": method,
|
||||
"params": params,
|
||||
});
|
||||
|
||||
// Encode and send
|
||||
let payload_str = serde_json::to_string(&inner)
|
||||
.map_err(ClientError::Serialization)?;
|
||||
let payload_b64 = BASE64_STANDARD.encode(payload_str.as_bytes());
|
||||
|
||||
let _msg_id = self.hub.mycelium
|
||||
.push_message(self.build_dst(), &self.hub.topic, &payload_b64)
|
||||
.await
|
||||
.map_err(|e| ClientError::from(e))?;
|
||||
|
||||
// Wait for reply
|
||||
let reply = timeout(Duration::from_secs(self.timeout_secs), rx)
|
||||
.await
|
||||
.map_err(|_| ClientError::Server { message: "Timeout waiting for reply".to_string() })?
|
||||
.map_err(|_| ClientError::Server { message: "Reply channel closed".to_string() })?;
|
||||
|
||||
// Check for JSON-RPC error
|
||||
if let Some(error) = reply.get("error") {
|
||||
let msg = error.get("message")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown error");
|
||||
return Err(ClientError::Server { message: msg.to_string() });
|
||||
}
|
||||
|
||||
// Extract result
|
||||
reply.get("result")
|
||||
.cloned()
|
||||
.ok_or_else(|| ClientError::Server { message: "Missing result in reply".to_string() })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user