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?.
This commit is contained in:
@@ -29,9 +29,9 @@ impl ServiceClient for HydraClient {
|
||||
}
|
||||
|
||||
impl HydraClient {
|
||||
/// Build a HydraClient from domain (e.g. `https://auth.{domain}`).
|
||||
/// Build a HydraClient from domain (e.g. `https://hydra.{domain}`).
|
||||
pub fn connect(domain: &str) -> Self {
|
||||
let base_url = format!("https://auth.{domain}");
|
||||
let base_url = format!("https://hydra.{domain}");
|
||||
Self::from_parts(base_url, AuthMethod::None)
|
||||
}
|
||||
|
||||
@@ -467,7 +467,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_connect_url() {
|
||||
let c = HydraClient::connect("sunbeam.pt");
|
||||
assert_eq!(c.base_url(), "https://auth.sunbeam.pt");
|
||||
assert_eq!(c.base_url(), "https://hydra.sunbeam.pt");
|
||||
assert_eq!(c.service_name(), "hydra");
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use crate::error::{Result, ResultExt, SunbeamError};
|
||||
use reqwest::Method;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::sync::OnceLock;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AuthMethod
|
||||
@@ -222,51 +222,51 @@ impl HttpTransport {
|
||||
/// Unified entry point for all service clients.
|
||||
///
|
||||
/// Lazily constructs and caches per-service clients from the active config
|
||||
/// context. Each accessor returns a `&Client` reference, constructing on
|
||||
/// first call via [`OnceLock`].
|
||||
/// context. Each accessor resolves auth and returns a `&Client` reference,
|
||||
/// constructing on first call via [`OnceCell`] (async-aware).
|
||||
///
|
||||
/// Auth is resolved per-client:
|
||||
/// - SSO bearer (`get_token()`) — admin APIs, Matrix, La Suite, OpenSearch
|
||||
/// - Gitea PAT (`get_gitea_token()`) — Gitea
|
||||
/// - None — Prometheus, Loki, S3, LiveKit
|
||||
pub struct SunbeamClient {
|
||||
ctx: crate::config::Context,
|
||||
domain: String,
|
||||
// Phase 1
|
||||
#[cfg(feature = "identity")]
|
||||
kratos: OnceLock<crate::identity::KratosClient>,
|
||||
kratos: OnceCell<crate::identity::KratosClient>,
|
||||
#[cfg(feature = "identity")]
|
||||
hydra: OnceLock<crate::auth::hydra::HydraClient>,
|
||||
// Phase 2
|
||||
hydra: OnceCell<crate::auth::hydra::HydraClient>,
|
||||
#[cfg(feature = "gitea")]
|
||||
gitea: OnceLock<crate::gitea::GiteaClient>,
|
||||
// Phase 3
|
||||
gitea: OnceCell<crate::gitea::GiteaClient>,
|
||||
#[cfg(feature = "matrix")]
|
||||
matrix: OnceLock<crate::matrix::MatrixClient>,
|
||||
matrix: OnceCell<crate::matrix::MatrixClient>,
|
||||
#[cfg(feature = "opensearch")]
|
||||
opensearch: OnceLock<crate::search::OpenSearchClient>,
|
||||
opensearch: OnceCell<crate::search::OpenSearchClient>,
|
||||
#[cfg(feature = "s3")]
|
||||
s3: OnceLock<crate::storage::S3Client>,
|
||||
s3: OnceCell<crate::storage::S3Client>,
|
||||
#[cfg(feature = "livekit")]
|
||||
livekit: OnceLock<crate::media::LiveKitClient>,
|
||||
livekit: OnceCell<crate::media::LiveKitClient>,
|
||||
#[cfg(feature = "monitoring")]
|
||||
prometheus: OnceLock<crate::monitoring::PrometheusClient>,
|
||||
prometheus: OnceCell<crate::monitoring::PrometheusClient>,
|
||||
#[cfg(feature = "monitoring")]
|
||||
loki: OnceLock<crate::monitoring::LokiClient>,
|
||||
loki: OnceCell<crate::monitoring::LokiClient>,
|
||||
#[cfg(feature = "monitoring")]
|
||||
grafana: OnceLock<crate::monitoring::GrafanaClient>,
|
||||
// Phase 4
|
||||
grafana: OnceCell<crate::monitoring::GrafanaClient>,
|
||||
#[cfg(feature = "lasuite")]
|
||||
people: OnceLock<crate::lasuite::PeopleClient>,
|
||||
people: OnceCell<crate::lasuite::PeopleClient>,
|
||||
#[cfg(feature = "lasuite")]
|
||||
docs: OnceLock<crate::lasuite::DocsClient>,
|
||||
docs: OnceCell<crate::lasuite::DocsClient>,
|
||||
#[cfg(feature = "lasuite")]
|
||||
meet: OnceLock<crate::lasuite::MeetClient>,
|
||||
meet: OnceCell<crate::lasuite::MeetClient>,
|
||||
#[cfg(feature = "lasuite")]
|
||||
drive: OnceLock<crate::lasuite::DriveClient>,
|
||||
drive: OnceCell<crate::lasuite::DriveClient>,
|
||||
#[cfg(feature = "lasuite")]
|
||||
messages: OnceLock<crate::lasuite::MessagesClient>,
|
||||
messages: OnceCell<crate::lasuite::MessagesClient>,
|
||||
#[cfg(feature = "lasuite")]
|
||||
calendars: OnceLock<crate::lasuite::CalendarsClient>,
|
||||
calendars: OnceCell<crate::lasuite::CalendarsClient>,
|
||||
#[cfg(feature = "lasuite")]
|
||||
find: OnceLock<crate::lasuite::FindClient>,
|
||||
// Bao/Planka stay in their existing modules
|
||||
bao: OnceLock<crate::openbao::BaoClient>,
|
||||
find: OnceCell<crate::lasuite::FindClient>,
|
||||
bao: OnceCell<crate::openbao::BaoClient>,
|
||||
}
|
||||
|
||||
impl SunbeamClient {
|
||||
@@ -276,40 +276,40 @@ impl SunbeamClient {
|
||||
domain: ctx.domain.clone(),
|
||||
ctx: ctx.clone(),
|
||||
#[cfg(feature = "identity")]
|
||||
kratos: OnceLock::new(),
|
||||
kratos: OnceCell::new(),
|
||||
#[cfg(feature = "identity")]
|
||||
hydra: OnceLock::new(),
|
||||
hydra: OnceCell::new(),
|
||||
#[cfg(feature = "gitea")]
|
||||
gitea: OnceLock::new(),
|
||||
gitea: OnceCell::new(),
|
||||
#[cfg(feature = "matrix")]
|
||||
matrix: OnceLock::new(),
|
||||
matrix: OnceCell::new(),
|
||||
#[cfg(feature = "opensearch")]
|
||||
opensearch: OnceLock::new(),
|
||||
opensearch: OnceCell::new(),
|
||||
#[cfg(feature = "s3")]
|
||||
s3: OnceLock::new(),
|
||||
s3: OnceCell::new(),
|
||||
#[cfg(feature = "livekit")]
|
||||
livekit: OnceLock::new(),
|
||||
livekit: OnceCell::new(),
|
||||
#[cfg(feature = "monitoring")]
|
||||
prometheus: OnceLock::new(),
|
||||
prometheus: OnceCell::new(),
|
||||
#[cfg(feature = "monitoring")]
|
||||
loki: OnceLock::new(),
|
||||
loki: OnceCell::new(),
|
||||
#[cfg(feature = "monitoring")]
|
||||
grafana: OnceLock::new(),
|
||||
grafana: OnceCell::new(),
|
||||
#[cfg(feature = "lasuite")]
|
||||
people: OnceLock::new(),
|
||||
people: OnceCell::new(),
|
||||
#[cfg(feature = "lasuite")]
|
||||
docs: OnceLock::new(),
|
||||
docs: OnceCell::new(),
|
||||
#[cfg(feature = "lasuite")]
|
||||
meet: OnceLock::new(),
|
||||
meet: OnceCell::new(),
|
||||
#[cfg(feature = "lasuite")]
|
||||
drive: OnceLock::new(),
|
||||
drive: OnceCell::new(),
|
||||
#[cfg(feature = "lasuite")]
|
||||
messages: OnceLock::new(),
|
||||
messages: OnceCell::new(),
|
||||
#[cfg(feature = "lasuite")]
|
||||
calendars: OnceLock::new(),
|
||||
calendars: OnceCell::new(),
|
||||
#[cfg(feature = "lasuite")]
|
||||
find: OnceLock::new(),
|
||||
bao: OnceLock::new(),
|
||||
find: OnceCell::new(),
|
||||
bao: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,131 +323,172 @@ impl SunbeamClient {
|
||||
&self.ctx
|
||||
}
|
||||
|
||||
// -- Lazy accessors (each feature-gated) --------------------------------
|
||||
// -- Auth helpers --------------------------------------------------------
|
||||
|
||||
/// Get cached SSO bearer token (from `sunbeam auth sso`).
|
||||
async fn sso_token(&self) -> Result<String> {
|
||||
crate::auth::get_token().await
|
||||
}
|
||||
|
||||
/// Get cached Gitea PAT (from `sunbeam auth git`).
|
||||
fn gitea_token(&self) -> Result<String> {
|
||||
crate::auth::get_gitea_token()
|
||||
}
|
||||
|
||||
// -- Lazy async accessors (each feature-gated) ---------------------------
|
||||
//
|
||||
// Each accessor resolves the appropriate auth and constructs the client
|
||||
// with from_parts(url, auth). Cached after first call.
|
||||
|
||||
#[cfg(feature = "identity")]
|
||||
pub fn kratos(&self) -> &crate::identity::KratosClient {
|
||||
self.kratos.get_or_init(|| {
|
||||
crate::identity::KratosClient::connect(&self.domain)
|
||||
})
|
||||
pub async fn kratos(&self) -> Result<&crate::identity::KratosClient> {
|
||||
self.kratos.get_or_try_init(|| async {
|
||||
let token = self.sso_token().await?;
|
||||
let url = format!("https://id.{}", self.domain);
|
||||
Ok(crate::identity::KratosClient::from_parts(url, AuthMethod::Bearer(token)))
|
||||
}).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "identity")]
|
||||
pub fn hydra(&self) -> &crate::auth::hydra::HydraClient {
|
||||
self.hydra.get_or_init(|| {
|
||||
crate::auth::hydra::HydraClient::connect(&self.domain)
|
||||
})
|
||||
pub async fn hydra(&self) -> Result<&crate::auth::hydra::HydraClient> {
|
||||
self.hydra.get_or_try_init(|| async {
|
||||
let token = self.sso_token().await?;
|
||||
let url = format!("https://hydra.{}", self.domain);
|
||||
Ok(crate::auth::hydra::HydraClient::from_parts(url, AuthMethod::Bearer(token)))
|
||||
}).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "gitea")]
|
||||
pub fn gitea(&self) -> &crate::gitea::GiteaClient {
|
||||
self.gitea.get_or_init(|| {
|
||||
crate::gitea::GiteaClient::connect(&self.domain)
|
||||
})
|
||||
pub async fn gitea(&self) -> Result<&crate::gitea::GiteaClient> {
|
||||
self.gitea.get_or_try_init(|| async {
|
||||
let token = self.gitea_token()?;
|
||||
let url = format!("https://src.{}/api/v1", self.domain);
|
||||
Ok(crate::gitea::GiteaClient::from_parts(url, AuthMethod::Token(token)))
|
||||
}).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "matrix")]
|
||||
pub fn matrix(&self) -> &crate::matrix::MatrixClient {
|
||||
self.matrix.get_or_init(|| {
|
||||
crate::matrix::MatrixClient::connect(&self.domain)
|
||||
})
|
||||
pub async fn matrix(&self) -> Result<&crate::matrix::MatrixClient> {
|
||||
self.matrix.get_or_try_init(|| async {
|
||||
let token = self.sso_token().await?;
|
||||
let url = format!("https://messages.{}/_matrix", self.domain);
|
||||
Ok(crate::matrix::MatrixClient::from_parts(url, AuthMethod::Bearer(token)))
|
||||
}).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "opensearch")]
|
||||
pub fn opensearch(&self) -> &crate::search::OpenSearchClient {
|
||||
self.opensearch.get_or_init(|| {
|
||||
crate::search::OpenSearchClient::connect(&self.domain)
|
||||
})
|
||||
pub async fn opensearch(&self) -> Result<&crate::search::OpenSearchClient> {
|
||||
self.opensearch.get_or_try_init(|| async {
|
||||
let token = self.sso_token().await?;
|
||||
let url = format!("https://search.{}", self.domain);
|
||||
Ok(crate::search::OpenSearchClient::from_parts(url, AuthMethod::Bearer(token)))
|
||||
}).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "s3")]
|
||||
pub fn s3(&self) -> &crate::storage::S3Client {
|
||||
self.s3.get_or_init(|| {
|
||||
crate::storage::S3Client::connect(&self.domain)
|
||||
})
|
||||
pub async fn s3(&self) -> Result<&crate::storage::S3Client> {
|
||||
self.s3.get_or_try_init(|| async {
|
||||
Ok(crate::storage::S3Client::connect(&self.domain))
|
||||
}).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "livekit")]
|
||||
pub fn livekit(&self) -> &crate::media::LiveKitClient {
|
||||
self.livekit.get_or_init(|| {
|
||||
crate::media::LiveKitClient::connect(&self.domain)
|
||||
})
|
||||
pub async fn livekit(&self) -> Result<&crate::media::LiveKitClient> {
|
||||
self.livekit.get_or_try_init(|| async {
|
||||
Ok(crate::media::LiveKitClient::connect(&self.domain))
|
||||
}).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "monitoring")]
|
||||
pub fn prometheus(&self) -> &crate::monitoring::PrometheusClient {
|
||||
self.prometheus.get_or_init(|| {
|
||||
crate::monitoring::PrometheusClient::connect(&self.domain)
|
||||
})
|
||||
pub async fn prometheus(&self) -> Result<&crate::monitoring::PrometheusClient> {
|
||||
self.prometheus.get_or_try_init(|| async {
|
||||
Ok(crate::monitoring::PrometheusClient::connect(&self.domain))
|
||||
}).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "monitoring")]
|
||||
pub fn loki(&self) -> &crate::monitoring::LokiClient {
|
||||
self.loki.get_or_init(|| {
|
||||
crate::monitoring::LokiClient::connect(&self.domain)
|
||||
})
|
||||
pub async fn loki(&self) -> Result<&crate::monitoring::LokiClient> {
|
||||
self.loki.get_or_try_init(|| async {
|
||||
Ok(crate::monitoring::LokiClient::connect(&self.domain))
|
||||
}).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "monitoring")]
|
||||
pub fn grafana(&self) -> &crate::monitoring::GrafanaClient {
|
||||
self.grafana.get_or_init(|| {
|
||||
crate::monitoring::GrafanaClient::connect(&self.domain)
|
||||
})
|
||||
pub async fn grafana(&self) -> Result<&crate::monitoring::GrafanaClient> {
|
||||
self.grafana.get_or_try_init(|| async {
|
||||
Ok(crate::monitoring::GrafanaClient::connect(&self.domain))
|
||||
}).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "lasuite")]
|
||||
pub fn people(&self) -> &crate::lasuite::PeopleClient {
|
||||
self.people.get_or_init(|| {
|
||||
crate::lasuite::PeopleClient::connect(&self.domain)
|
||||
})
|
||||
pub async fn people(&self) -> Result<&crate::lasuite::PeopleClient> {
|
||||
self.people.get_or_try_init(|| async {
|
||||
let token = self.sso_token().await?;
|
||||
let url = format!("https://people.{}/api/v1.0", self.domain);
|
||||
Ok(crate::lasuite::PeopleClient::from_parts(url, AuthMethod::Bearer(token)))
|
||||
}).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "lasuite")]
|
||||
pub fn docs(&self) -> &crate::lasuite::DocsClient {
|
||||
self.docs.get_or_init(|| {
|
||||
crate::lasuite::DocsClient::connect(&self.domain)
|
||||
})
|
||||
pub async fn docs(&self) -> Result<&crate::lasuite::DocsClient> {
|
||||
self.docs.get_or_try_init(|| async {
|
||||
let token = self.sso_token().await?;
|
||||
let url = format!("https://docs.{}/api/v1.0", self.domain);
|
||||
Ok(crate::lasuite::DocsClient::from_parts(url, AuthMethod::Bearer(token)))
|
||||
}).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "lasuite")]
|
||||
pub fn meet(&self) -> &crate::lasuite::MeetClient {
|
||||
self.meet.get_or_init(|| {
|
||||
crate::lasuite::MeetClient::connect(&self.domain)
|
||||
})
|
||||
pub async fn meet(&self) -> Result<&crate::lasuite::MeetClient> {
|
||||
self.meet.get_or_try_init(|| async {
|
||||
let token = self.sso_token().await?;
|
||||
let url = format!("https://meet.{}/api/v1.0", self.domain);
|
||||
Ok(crate::lasuite::MeetClient::from_parts(url, AuthMethod::Bearer(token)))
|
||||
}).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "lasuite")]
|
||||
pub fn drive(&self) -> &crate::lasuite::DriveClient {
|
||||
self.drive.get_or_init(|| {
|
||||
crate::lasuite::DriveClient::connect(&self.domain)
|
||||
})
|
||||
pub async fn drive(&self) -> Result<&crate::lasuite::DriveClient> {
|
||||
self.drive.get_or_try_init(|| async {
|
||||
let token = self.sso_token().await?;
|
||||
let url = format!("https://drive.{}/api/v1.0", self.domain);
|
||||
Ok(crate::lasuite::DriveClient::from_parts(url, AuthMethod::Bearer(token)))
|
||||
}).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "lasuite")]
|
||||
pub fn messages(&self) -> &crate::lasuite::MessagesClient {
|
||||
self.messages.get_or_init(|| {
|
||||
crate::lasuite::MessagesClient::connect(&self.domain)
|
||||
})
|
||||
pub async fn messages(&self) -> Result<&crate::lasuite::MessagesClient> {
|
||||
self.messages.get_or_try_init(|| async {
|
||||
let token = self.sso_token().await?;
|
||||
let url = format!("https://mail.{}/api/v1.0", self.domain);
|
||||
Ok(crate::lasuite::MessagesClient::from_parts(url, AuthMethod::Bearer(token)))
|
||||
}).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "lasuite")]
|
||||
pub fn calendars(&self) -> &crate::lasuite::CalendarsClient {
|
||||
self.calendars.get_or_init(|| {
|
||||
crate::lasuite::CalendarsClient::connect(&self.domain)
|
||||
})
|
||||
pub async fn calendars(&self) -> Result<&crate::lasuite::CalendarsClient> {
|
||||
self.calendars.get_or_try_init(|| async {
|
||||
let token = self.sso_token().await?;
|
||||
let url = format!("https://calendar.{}/api/v1.0", self.domain);
|
||||
Ok(crate::lasuite::CalendarsClient::from_parts(url, AuthMethod::Bearer(token)))
|
||||
}).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "lasuite")]
|
||||
pub fn find(&self) -> &crate::lasuite::FindClient {
|
||||
self.find.get_or_init(|| {
|
||||
crate::lasuite::FindClient::connect(&self.domain)
|
||||
})
|
||||
pub async fn find(&self) -> Result<&crate::lasuite::FindClient> {
|
||||
self.find.get_or_try_init(|| async {
|
||||
let token = self.sso_token().await?;
|
||||
let url = format!("https://find.{}/api/v1.0", self.domain);
|
||||
Ok(crate::lasuite::FindClient::from_parts(url, AuthMethod::Bearer(token)))
|
||||
}).await
|
||||
}
|
||||
|
||||
pub fn bao(&self, base_url: &str) -> &crate::openbao::BaoClient {
|
||||
self.bao.get_or_init(|| {
|
||||
crate::openbao::BaoClient::new(base_url)
|
||||
})
|
||||
pub async fn bao(&self) -> Result<&crate::openbao::BaoClient> {
|
||||
self.bao.get_or_try_init(|| async {
|
||||
let token = self.sso_token().await?;
|
||||
let url = format!("https://vault.{}", self.domain);
|
||||
Ok(crate::openbao::BaoClient::with_token(&url, &token))
|
||||
}).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
use clap::Subcommand;
|
||||
|
||||
use crate::client::SunbeamClient;
|
||||
use crate::error::{Result, SunbeamError};
|
||||
use crate::gitea::types::*;
|
||||
use crate::gitea::GiteaClient;
|
||||
use crate::output::{render, render_list, read_json_input, OutputFormat};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -435,7 +435,8 @@ fn notification_row(n: &Notification) -> Vec<String> {
|
||||
// Dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn dispatch(cmd: VcsCommand, client: &GiteaClient, fmt: OutputFormat) -> Result<()> {
|
||||
pub async fn dispatch(cmd: VcsCommand, client: &SunbeamClient, fmt: OutputFormat) -> Result<()> {
|
||||
let client = client.gitea().await?;
|
||||
match cmd {
|
||||
// -- Repo -----------------------------------------------------------
|
||||
VcsCommand::Repo { action } => match action {
|
||||
|
||||
@@ -349,7 +349,7 @@ pub async fn dispatch(
|
||||
AuthCommand::Courier { action } => dispatch_courier(action, client, output).await,
|
||||
// -- Kratos: Health -----------------------------------------------------
|
||||
AuthCommand::Health => {
|
||||
let status = client.kratos().alive().await?;
|
||||
let status = client.kratos().await?.alive().await?;
|
||||
output::render(&status, output)
|
||||
}
|
||||
// -- Hydra: Client ------------------------------------------------------
|
||||
@@ -384,7 +384,7 @@ async fn dispatch_identity(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let kratos = client.kratos();
|
||||
let kratos = client.kratos().await?;
|
||||
match action {
|
||||
IdentityAction::List { page, page_size } => {
|
||||
let items = kratos.list_identities(page, page_size).await?;
|
||||
@@ -437,7 +437,7 @@ async fn dispatch_session(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let kratos = client.kratos();
|
||||
let kratos = client.kratos().await?;
|
||||
match action {
|
||||
SessionAction::List {
|
||||
page_size,
|
||||
@@ -486,7 +486,7 @@ async fn dispatch_recovery(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let kratos = client.kratos();
|
||||
let kratos = client.kratos().await?;
|
||||
match action {
|
||||
RecoveryAction::CreateCode { id, expires_in } => {
|
||||
let item = kratos
|
||||
@@ -512,7 +512,7 @@ async fn dispatch_schema(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let kratos = client.kratos();
|
||||
let kratos = client.kratos().await?;
|
||||
match action {
|
||||
SchemaAction::List => {
|
||||
let items = kratos.list_schemas().await?;
|
||||
@@ -539,7 +539,7 @@ async fn dispatch_courier(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let kratos = client.kratos();
|
||||
let kratos = client.kratos().await?;
|
||||
match action {
|
||||
CourierAction::List {
|
||||
page_size,
|
||||
@@ -579,7 +579,7 @@ async fn dispatch_client(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let hydra = client.hydra();
|
||||
let hydra = client.hydra().await?;
|
||||
match action {
|
||||
ClientAction::List { limit, offset } => {
|
||||
let items = hydra.list_clients(limit, offset).await?;
|
||||
@@ -631,7 +631,7 @@ async fn dispatch_jwk(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let hydra = client.hydra();
|
||||
let hydra = client.hydra().await?;
|
||||
match action {
|
||||
JwkAction::List { set_name } => {
|
||||
let item = hydra.get_jwk_set(&set_name).await?;
|
||||
@@ -665,7 +665,7 @@ async fn dispatch_issuer(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let hydra = client.hydra();
|
||||
let hydra = client.hydra().await?;
|
||||
match action {
|
||||
IssuerAction::List => {
|
||||
let items = hydra.list_trusted_issuers().await?;
|
||||
@@ -711,7 +711,7 @@ async fn dispatch_token(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let hydra = client.hydra();
|
||||
let hydra = client.hydra().await?;
|
||||
match action {
|
||||
TokenAction::Introspect { token } => {
|
||||
let item = hydra.introspect_token(&token).await?;
|
||||
|
||||
@@ -6,44 +6,6 @@ use crate::client::SunbeamClient;
|
||||
use crate::error::Result;
|
||||
use crate::output::{self, OutputFormat};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Helper: build an authenticated La Suite client
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async fn people_client(domain: &str) -> Result<super::PeopleClient> {
|
||||
let token = crate::auth::get_token().await?;
|
||||
Ok(super::PeopleClient::connect(domain).with_token(&token))
|
||||
}
|
||||
|
||||
async fn docs_client(domain: &str) -> Result<super::DocsClient> {
|
||||
let token = crate::auth::get_token().await?;
|
||||
Ok(super::DocsClient::connect(domain).with_token(&token))
|
||||
}
|
||||
|
||||
async fn meet_client(domain: &str) -> Result<super::MeetClient> {
|
||||
let token = crate::auth::get_token().await?;
|
||||
Ok(super::MeetClient::connect(domain).with_token(&token))
|
||||
}
|
||||
|
||||
async fn drive_client(domain: &str) -> Result<super::DriveClient> {
|
||||
let token = crate::auth::get_token().await?;
|
||||
Ok(super::DriveClient::connect(domain).with_token(&token))
|
||||
}
|
||||
|
||||
async fn messages_client(domain: &str) -> Result<super::MessagesClient> {
|
||||
let token = crate::auth::get_token().await?;
|
||||
Ok(super::MessagesClient::connect(domain).with_token(&token))
|
||||
}
|
||||
|
||||
async fn calendars_client(domain: &str) -> Result<super::CalendarsClient> {
|
||||
let token = crate::auth::get_token().await?;
|
||||
Ok(super::CalendarsClient::connect(domain).with_token(&token))
|
||||
}
|
||||
|
||||
async fn find_client(domain: &str) -> Result<super::FindClient> {
|
||||
let token = crate::auth::get_token().await?;
|
||||
Ok(super::FindClient::connect(domain).with_token(&token))
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// People
|
||||
@@ -143,7 +105,7 @@ pub async fn dispatch_people(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let people = people_client(client.domain()).await?;
|
||||
let people = client.people().await?;
|
||||
match cmd {
|
||||
PeopleCommand::Contact { action } => match action {
|
||||
ContactAction::List { page } => {
|
||||
@@ -346,7 +308,7 @@ pub async fn dispatch_docs(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let docs = docs_client(client.domain()).await?;
|
||||
let docs = client.docs().await?;
|
||||
match cmd {
|
||||
DocsCommand::Document { action } => match action {
|
||||
DocumentAction::List { page } => {
|
||||
@@ -498,7 +460,7 @@ pub async fn dispatch_meet(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let meet = meet_client(client.domain()).await?;
|
||||
let meet = client.meet().await?;
|
||||
match cmd {
|
||||
MeetCommand::Room { action } => match action {
|
||||
RoomAction::List { page } => {
|
||||
@@ -645,7 +607,7 @@ pub async fn dispatch_drive(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let drive = drive_client(client.domain()).await?;
|
||||
let drive = client.drive().await?;
|
||||
match cmd {
|
||||
DriveCommand::File { action } => match action {
|
||||
FileAction::List { page } => {
|
||||
@@ -823,7 +785,7 @@ pub async fn dispatch_mail(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let mail = messages_client(client.domain()).await?;
|
||||
let mail = client.messages().await?;
|
||||
match cmd {
|
||||
MailCommand::Mailbox { action } => match action {
|
||||
MailboxAction::List => {
|
||||
@@ -1013,7 +975,7 @@ pub async fn dispatch_cal(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let cal = calendars_client(client.domain()).await?;
|
||||
let cal = client.calendars().await?;
|
||||
match cmd {
|
||||
CalCommand::Calendar { action } => match action {
|
||||
CalendarAction::List => {
|
||||
@@ -1124,7 +1086,7 @@ pub async fn dispatch_find(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let find = find_client(client.domain()).await?;
|
||||
let find = client.find().await?;
|
||||
match cmd {
|
||||
FindCommand::Search { query, page } => {
|
||||
let page_data = find.search(&query, page).await?;
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
//! CLI dispatch for Matrix chat commands.
|
||||
|
||||
use crate::client::SunbeamClient;
|
||||
use crate::error::Result;
|
||||
use crate::output::{self, OutputFormat};
|
||||
use clap::Subcommand;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Construct a [`MatrixClient`] with a valid access token from the credential
|
||||
/// cache. Fails if the user is not logged in.
|
||||
async fn matrix_with_token(domain: &str) -> Result<super::MatrixClient> {
|
||||
let token = crate::auth::get_token().await?;
|
||||
let mut m = super::MatrixClient::connect(domain);
|
||||
m.set_token(&token);
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Command tree
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -343,8 +331,8 @@ pub enum UserAction {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Dispatch a parsed [`ChatCommand`] against the Matrix homeserver.
|
||||
pub async fn dispatch(domain: &str, format: OutputFormat, cmd: ChatCommand) -> Result<()> {
|
||||
let m = matrix_with_token(domain).await?;
|
||||
pub async fn dispatch(client: &SunbeamClient, format: OutputFormat, cmd: ChatCommand) -> Result<()> {
|
||||
let m = client.matrix().await?;
|
||||
|
||||
match cmd {
|
||||
// -- Whoami ---------------------------------------------------------
|
||||
|
||||
@@ -32,9 +32,9 @@ impl ServiceClient for MatrixClient {
|
||||
}
|
||||
|
||||
impl MatrixClient {
|
||||
/// Build a MatrixClient from domain (e.g. `https://matrix.{domain}/_matrix`).
|
||||
/// Build a MatrixClient from domain (e.g. `https://messages.{domain}/_matrix`).
|
||||
pub fn connect(domain: &str) -> Self {
|
||||
let base_url = format!("https://matrix.{domain}/_matrix");
|
||||
let base_url = format!("https://messages.{domain}/_matrix");
|
||||
Self::from_parts(base_url, AuthMethod::Bearer(String::new()))
|
||||
}
|
||||
|
||||
@@ -1204,7 +1204,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_connect_url() {
|
||||
let c = MatrixClient::connect("sunbeam.pt");
|
||||
assert_eq!(c.base_url(), "https://matrix.sunbeam.pt/_matrix");
|
||||
assert_eq!(c.base_url(), "https://messages.sunbeam.pt/_matrix");
|
||||
assert_eq!(c.service_name(), "matrix");
|
||||
}
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ async fn dispatch_room(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let lk = client.livekit();
|
||||
let lk = client.livekit().await?;
|
||||
match action {
|
||||
RoomAction::List => {
|
||||
let resp = lk.list_rooms().await?;
|
||||
@@ -227,7 +227,7 @@ async fn dispatch_participant(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let lk = client.livekit();
|
||||
let lk = client.livekit().await?;
|
||||
match action {
|
||||
ParticipantAction::List { room } => {
|
||||
let resp = lk
|
||||
@@ -278,7 +278,7 @@ async fn dispatch_egress(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let lk = client.livekit();
|
||||
let lk = client.livekit().await?;
|
||||
match action {
|
||||
EgressAction::List { room } => {
|
||||
let resp = lk
|
||||
|
||||
@@ -425,7 +425,7 @@ async fn dispatch_prometheus(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let prom = client.prometheus();
|
||||
let prom = client.prometheus().await?;
|
||||
match action {
|
||||
PrometheusAction::Query { query, time } => {
|
||||
let res = prom.query(&query, time.as_deref()).await?;
|
||||
@@ -511,7 +511,7 @@ async fn dispatch_loki(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let loki = client.loki();
|
||||
let loki = client.loki().await?;
|
||||
match action {
|
||||
LokiAction::Query { query, limit, time } => {
|
||||
let res = loki.query(&query, limit, time.as_deref()).await?;
|
||||
@@ -631,7 +631,7 @@ async fn dispatch_grafana_dashboard(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let grafana = client.grafana();
|
||||
let grafana = client.grafana().await?;
|
||||
match action {
|
||||
GrafanaDashboardAction::List => {
|
||||
let items = grafana.list_dashboards().await?;
|
||||
@@ -696,7 +696,7 @@ async fn dispatch_grafana_datasource(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let grafana = client.grafana();
|
||||
let grafana = client.grafana().await?;
|
||||
match action {
|
||||
GrafanaDatasourceAction::List => {
|
||||
let items = grafana.list_datasources().await?;
|
||||
@@ -746,7 +746,7 @@ async fn dispatch_grafana_folder(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let grafana = client.grafana();
|
||||
let grafana = client.grafana().await?;
|
||||
match action {
|
||||
GrafanaFolderAction::List => {
|
||||
let items = grafana.list_folders().await?;
|
||||
@@ -794,7 +794,7 @@ async fn dispatch_grafana_annotation(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let grafana = client.grafana();
|
||||
let grafana = client.grafana().await?;
|
||||
match action {
|
||||
GrafanaAnnotationAction::List { params } => {
|
||||
let items = grafana.list_annotations(params.as_deref()).await?;
|
||||
@@ -833,7 +833,7 @@ async fn dispatch_grafana_alert(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let grafana = client.grafana();
|
||||
let grafana = client.grafana().await?;
|
||||
match action {
|
||||
GrafanaAlertAction::List => {
|
||||
let items = grafana.get_alert_rules().await?;
|
||||
@@ -879,7 +879,7 @@ async fn dispatch_grafana_org(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let grafana = client.grafana();
|
||||
let grafana = client.grafana().await?;
|
||||
match action {
|
||||
GrafanaOrgAction::Get => {
|
||||
let item = grafana.get_current_org().await?;
|
||||
|
||||
@@ -27,9 +27,9 @@ impl ServiceClient for GrafanaClient {
|
||||
}
|
||||
|
||||
impl GrafanaClient {
|
||||
/// Build a GrafanaClient from domain (e.g. `https://grafana.{domain}/api`).
|
||||
/// Build a GrafanaClient from domain (e.g. `https://metrics.{domain}/api`).
|
||||
pub fn connect(domain: &str) -> Self {
|
||||
let base_url = format!("https://grafana.{domain}/api");
|
||||
let base_url = format!("https://metrics.{domain}/api");
|
||||
Self::from_parts(base_url, AuthMethod::None)
|
||||
}
|
||||
|
||||
@@ -410,7 +410,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_connect_url() {
|
||||
let c = GrafanaClient::connect("sunbeam.pt");
|
||||
assert_eq!(c.base_url(), "https://grafana.sunbeam.pt/api");
|
||||
assert_eq!(c.base_url(), "https://metrics.sunbeam.pt/api");
|
||||
assert_eq!(c.service_name(), "grafana");
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ impl ServiceClient for LokiClient {
|
||||
}
|
||||
|
||||
impl LokiClient {
|
||||
/// Build a LokiClient from domain (e.g. `https://loki.{domain}/loki/api/v1`).
|
||||
/// Build a LokiClient from domain (e.g. `https://systemlogs.{domain}/loki/api/v1`).
|
||||
pub fn connect(domain: &str) -> Self {
|
||||
let base_url = format!("https://loki.{domain}/loki/api/v1");
|
||||
let base_url = format!("https://systemlogs.{domain}/loki/api/v1");
|
||||
Self::from_parts(base_url, AuthMethod::None)
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_connect_url() {
|
||||
let c = LokiClient::connect("sunbeam.pt");
|
||||
assert_eq!(c.base_url(), "https://loki.sunbeam.pt/loki/api/v1");
|
||||
assert_eq!(c.base_url(), "https://systemlogs.sunbeam.pt/loki/api/v1");
|
||||
assert_eq!(c.service_name(), "loki");
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ impl ServiceClient for PrometheusClient {
|
||||
}
|
||||
|
||||
impl PrometheusClient {
|
||||
/// Build a PrometheusClient from domain (e.g. `https://prometheus.{domain}/api/v1`).
|
||||
/// Build a PrometheusClient from domain (e.g. `https://systemmetrics.{domain}/api/v1`).
|
||||
pub fn connect(domain: &str) -> Self {
|
||||
let base_url = format!("https://prometheus.{domain}/api/v1");
|
||||
let base_url = format!("https://systemmetrics.{domain}/api/v1");
|
||||
Self::from_parts(base_url, AuthMethod::None)
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_connect_url() {
|
||||
let c = PrometheusClient::connect("sunbeam.pt");
|
||||
assert_eq!(c.base_url(), "https://prometheus.sunbeam.pt/api/v1");
|
||||
assert_eq!(c.base_url(), "https://systemmetrics.sunbeam.pt/api/v1");
|
||||
assert_eq!(c.service_name(), "prometheus");
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::collections::HashMap;
|
||||
|
||||
use clap::Subcommand;
|
||||
|
||||
use crate::client::SunbeamClient;
|
||||
use crate::error::Result;
|
||||
use crate::output::{self, OutputFormat};
|
||||
|
||||
@@ -226,9 +227,10 @@ fn read_text_input(flag: Option<&str>) -> Result<String> {
|
||||
|
||||
pub async fn dispatch(
|
||||
cmd: VaultCommand,
|
||||
bao: &super::BaoClient,
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let bao = client.bao().await?;
|
||||
match cmd {
|
||||
// -- Status ---------------------------------------------------------
|
||||
VaultCommand::Status => {
|
||||
|
||||
@@ -6,17 +6,6 @@ use serde_json::json;
|
||||
use crate::error::Result;
|
||||
use crate::output::{self, OutputFormat};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Client helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn os_client(domain: &str) -> Result<super::OpenSearchClient> {
|
||||
let token = crate::auth::get_token().await?;
|
||||
let mut c = super::OpenSearchClient::connect(domain);
|
||||
c.set_token(token);
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Top-level command enum
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -413,7 +402,7 @@ pub async fn dispatch(
|
||||
client: &crate::client::SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let c = os_client(client.domain()).await?;
|
||||
let c = client.opensearch().await?;
|
||||
|
||||
match cmd {
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
@@ -152,7 +152,7 @@ async fn dispatch_bucket(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let s3 = client.s3();
|
||||
let s3 = client.s3().await?;
|
||||
match action {
|
||||
BucketAction::List => {
|
||||
let resp = s3.list_buckets().await?;
|
||||
@@ -194,7 +194,7 @@ async fn dispatch_object(
|
||||
client: &SunbeamClient,
|
||||
fmt: OutputFormat,
|
||||
) -> Result<()> {
|
||||
let s3 = client.s3();
|
||||
let s3 = client.s3().await?;
|
||||
match action {
|
||||
ObjectAction::List {
|
||||
bucket,
|
||||
|
||||
Reference in New Issue
Block a user