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:
2026-03-22 18:57:22 +00:00
parent 34647e6bcb
commit faf525522c
17 changed files with 224 additions and 237 deletions

View File

@@ -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");
}

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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?;

View File

@@ -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?;

View File

@@ -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 ---------------------------------------------------------

View File

@@ -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");
}

View File

@@ -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

View File

@@ -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?;

View File

@@ -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");
}

View File

@@ -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");
}

View File

@@ -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");
}

View File

@@ -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 => {

View File

@@ -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 {
// -----------------------------------------------------------------

View File

@@ -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,