From faf525522c7ae263d75e03141c9bef6b4aff10ad Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Sun, 22 Mar 2026 18:57:22 +0000 Subject: [PATCH] feat: async SunbeamClient factory with unified auth resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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?. --- Cargo.lock | 4 +- sunbeam-sdk/src/auth/hydra/mod.rs | 6 +- sunbeam-sdk/src/client.rs | 275 +++++++++++++---------- sunbeam-sdk/src/gitea/cli.rs | 5 +- sunbeam-sdk/src/identity/cli.rs | 20 +- sunbeam-sdk/src/lasuite/cli.rs | 52 +---- sunbeam-sdk/src/matrix/cli.rs | 18 +- sunbeam-sdk/src/matrix/mod.rs | 6 +- sunbeam-sdk/src/media/cli.rs | 6 +- sunbeam-sdk/src/monitoring/cli.rs | 16 +- sunbeam-sdk/src/monitoring/grafana.rs | 6 +- sunbeam-sdk/src/monitoring/loki.rs | 6 +- sunbeam-sdk/src/monitoring/prometheus.rs | 6 +- sunbeam-sdk/src/openbao/cli.rs | 4 +- sunbeam-sdk/src/search/cli.rs | 13 +- sunbeam-sdk/src/storage/cli.rs | 4 +- sunbeam/src/cli.rs | 14 +- 17 files changed, 224 insertions(+), 237 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b2331a..2fa0b49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3469,7 +3469,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sunbeam" -version = "1.0.0" +version = "1.0.1" dependencies = [ "chrono", "clap", @@ -3482,7 +3482,7 @@ dependencies = [ [[package]] name = "sunbeam-sdk" -version = "1.0.0" +version = "1.0.1" dependencies = [ "base64", "bytes", diff --git a/sunbeam-sdk/src/auth/hydra/mod.rs b/sunbeam-sdk/src/auth/hydra/mod.rs index 42fad64..702304b 100644 --- a/sunbeam-sdk/src/auth/hydra/mod.rs +++ b/sunbeam-sdk/src/auth/hydra/mod.rs @@ -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"); } diff --git a/sunbeam-sdk/src/client.rs b/sunbeam-sdk/src/client.rs index 668240f..5bc704a 100644 --- a/sunbeam-sdk/src/client.rs +++ b/sunbeam-sdk/src/client.rs @@ -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, + kratos: OnceCell, #[cfg(feature = "identity")] - hydra: OnceLock, - // Phase 2 + hydra: OnceCell, #[cfg(feature = "gitea")] - gitea: OnceLock, - // Phase 3 + gitea: OnceCell, #[cfg(feature = "matrix")] - matrix: OnceLock, + matrix: OnceCell, #[cfg(feature = "opensearch")] - opensearch: OnceLock, + opensearch: OnceCell, #[cfg(feature = "s3")] - s3: OnceLock, + s3: OnceCell, #[cfg(feature = "livekit")] - livekit: OnceLock, + livekit: OnceCell, #[cfg(feature = "monitoring")] - prometheus: OnceLock, + prometheus: OnceCell, #[cfg(feature = "monitoring")] - loki: OnceLock, + loki: OnceCell, #[cfg(feature = "monitoring")] - grafana: OnceLock, - // Phase 4 + grafana: OnceCell, #[cfg(feature = "lasuite")] - people: OnceLock, + people: OnceCell, #[cfg(feature = "lasuite")] - docs: OnceLock, + docs: OnceCell, #[cfg(feature = "lasuite")] - meet: OnceLock, + meet: OnceCell, #[cfg(feature = "lasuite")] - drive: OnceLock, + drive: OnceCell, #[cfg(feature = "lasuite")] - messages: OnceLock, + messages: OnceCell, #[cfg(feature = "lasuite")] - calendars: OnceLock, + calendars: OnceCell, #[cfg(feature = "lasuite")] - find: OnceLock, - // Bao/Planka stay in their existing modules - bao: OnceLock, + find: OnceCell, + bao: OnceCell, } 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 { + crate::auth::get_token().await + } + + /// Get cached Gitea PAT (from `sunbeam auth git`). + fn gitea_token(&self) -> Result { + 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 } } diff --git a/sunbeam-sdk/src/gitea/cli.rs b/sunbeam-sdk/src/gitea/cli.rs index 75dd967..73f52b2 100644 --- a/sunbeam-sdk/src/gitea/cli.rs +++ b/sunbeam-sdk/src/gitea/cli.rs @@ -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 { // 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 { diff --git a/sunbeam-sdk/src/identity/cli.rs b/sunbeam-sdk/src/identity/cli.rs index 5b8f84b..7f742dd 100644 --- a/sunbeam-sdk/src/identity/cli.rs +++ b/sunbeam-sdk/src/identity/cli.rs @@ -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?; diff --git a/sunbeam-sdk/src/lasuite/cli.rs b/sunbeam-sdk/src/lasuite/cli.rs index 3d3edaa..3c03005 100644 --- a/sunbeam-sdk/src/lasuite/cli.rs +++ b/sunbeam-sdk/src/lasuite/cli.rs @@ -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 { - let token = crate::auth::get_token().await?; - Ok(super::PeopleClient::connect(domain).with_token(&token)) -} - -async fn docs_client(domain: &str) -> Result { - let token = crate::auth::get_token().await?; - Ok(super::DocsClient::connect(domain).with_token(&token)) -} - -async fn meet_client(domain: &str) -> Result { - let token = crate::auth::get_token().await?; - Ok(super::MeetClient::connect(domain).with_token(&token)) -} - -async fn drive_client(domain: &str) -> Result { - let token = crate::auth::get_token().await?; - Ok(super::DriveClient::connect(domain).with_token(&token)) -} - -async fn messages_client(domain: &str) -> Result { - let token = crate::auth::get_token().await?; - Ok(super::MessagesClient::connect(domain).with_token(&token)) -} - -async fn calendars_client(domain: &str) -> Result { - let token = crate::auth::get_token().await?; - Ok(super::CalendarsClient::connect(domain).with_token(&token)) -} - -async fn find_client(domain: &str) -> Result { - 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?; diff --git a/sunbeam-sdk/src/matrix/cli.rs b/sunbeam-sdk/src/matrix/cli.rs index 572848d..98abffd 100644 --- a/sunbeam-sdk/src/matrix/cli.rs +++ b/sunbeam-sdk/src/matrix/cli.rs @@ -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 { - 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 --------------------------------------------------------- diff --git a/sunbeam-sdk/src/matrix/mod.rs b/sunbeam-sdk/src/matrix/mod.rs index e6dfd3b..bfb075f 100644 --- a/sunbeam-sdk/src/matrix/mod.rs +++ b/sunbeam-sdk/src/matrix/mod.rs @@ -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"); } diff --git a/sunbeam-sdk/src/media/cli.rs b/sunbeam-sdk/src/media/cli.rs index 0755dae..da79695 100644 --- a/sunbeam-sdk/src/media/cli.rs +++ b/sunbeam-sdk/src/media/cli.rs @@ -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 diff --git a/sunbeam-sdk/src/monitoring/cli.rs b/sunbeam-sdk/src/monitoring/cli.rs index 9f8cd96..9dc7046 100644 --- a/sunbeam-sdk/src/monitoring/cli.rs +++ b/sunbeam-sdk/src/monitoring/cli.rs @@ -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?; diff --git a/sunbeam-sdk/src/monitoring/grafana.rs b/sunbeam-sdk/src/monitoring/grafana.rs index 73e0222..fe93a0d 100644 --- a/sunbeam-sdk/src/monitoring/grafana.rs +++ b/sunbeam-sdk/src/monitoring/grafana.rs @@ -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"); } diff --git a/sunbeam-sdk/src/monitoring/loki.rs b/sunbeam-sdk/src/monitoring/loki.rs index 07c5e9e..d5e1ccc 100644 --- a/sunbeam-sdk/src/monitoring/loki.rs +++ b/sunbeam-sdk/src/monitoring/loki.rs @@ -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"); } diff --git a/sunbeam-sdk/src/monitoring/prometheus.rs b/sunbeam-sdk/src/monitoring/prometheus.rs index 32a089f..7815e0d 100644 --- a/sunbeam-sdk/src/monitoring/prometheus.rs +++ b/sunbeam-sdk/src/monitoring/prometheus.rs @@ -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"); } diff --git a/sunbeam-sdk/src/openbao/cli.rs b/sunbeam-sdk/src/openbao/cli.rs index beb4986..bda4fd1 100644 --- a/sunbeam-sdk/src/openbao/cli.rs +++ b/sunbeam-sdk/src/openbao/cli.rs @@ -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 { pub async fn dispatch( cmd: VaultCommand, - bao: &super::BaoClient, + client: &SunbeamClient, fmt: OutputFormat, ) -> Result<()> { + let bao = client.bao().await?; match cmd { // -- Status --------------------------------------------------------- VaultCommand::Status => { diff --git a/sunbeam-sdk/src/search/cli.rs b/sunbeam-sdk/src/search/cli.rs index d7f53d4..121dc10 100644 --- a/sunbeam-sdk/src/search/cli.rs +++ b/sunbeam-sdk/src/search/cli.rs @@ -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 { - 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 { // ----------------------------------------------------------------- diff --git a/sunbeam-sdk/src/storage/cli.rs b/sunbeam-sdk/src/storage/cli.rs index 2a1ea54..4609af4 100644 --- a/sunbeam-sdk/src/storage/cli.rs +++ b/sunbeam-sdk/src/storage/cli.rs @@ -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, diff --git a/sunbeam/src/cli.rs b/sunbeam/src/cli.rs index 1980243..069cc86 100644 --- a/sunbeam/src/cli.rs +++ b/sunbeam/src/cli.rs @@ -927,12 +927,14 @@ pub async fn dispatch() -> Result<()> { let sc = sunbeam_sdk::client::SunbeamClient::from_context( &sunbeam_sdk::config::active_context(), ); - sunbeam_sdk::gitea::cli::dispatch(action, sc.gitea(), cli.output_format).await + sunbeam_sdk::gitea::cli::dispatch(action, &sc, cli.output_format).await } Some(Verb::Chat { action }) => { - let domain = sunbeam_sdk::config::active_context().domain.clone(); - sunbeam_sdk::matrix::cli::dispatch(&domain, cli.output_format, action).await + let sc = sunbeam_sdk::client::SunbeamClient::from_context( + &sunbeam_sdk::config::active_context(), + ); + sunbeam_sdk::matrix::cli::dispatch(&sc, cli.output_format, action).await } Some(Verb::Search { action }) => { @@ -964,8 +966,10 @@ pub async fn dispatch() -> Result<()> { } Some(Verb::Vault { action }) => { - let bao = sunbeam_sdk::openbao::BaoClient::new("http://127.0.0.1:8200"); - sunbeam_sdk::openbao::cli::dispatch(action, &bao, cli.output_format).await + let sc = sunbeam_sdk::client::SunbeamClient::from_context( + &sunbeam_sdk::config::active_context(), + ); + sunbeam_sdk::openbao::cli::dispatch(action, &sc, cli.output_format).await } Some(Verb::People { action }) => {