use std::{borrow::Cow, sync::Arc};
use anapaya_ead_client::client::{CrpcEndhostApiDiscoveryClient, EndhostApiDiscoveryClient};
use anapaya_ead_models::{EndhostApiGroup, EndhostApiInfo};
use snap_control::reexport::TokenSource;
use url::Url;
pub mod models {
pub use anapaya_ead_models::{EndhostApiGroup, EndhostApiInfo};
}
#[derive(Debug, thiserror::Error)]
#[error("failed to retrieve endhost APIs: {message}")]
#[non_exhaustive]
pub struct EndhostApiSourceError {
message: Cow<'static, str>,
transient: bool,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
impl EndhostApiSourceError {
#[must_use]
pub fn new(message: impl Into<Cow<'static, str>>, transient: bool) -> Self {
Self {
message: message.into(),
transient,
source: None,
}
}
#[must_use]
pub fn with_source(
message: impl Into<Cow<'static, str>>,
transient: bool,
source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
) -> Self {
Self {
message: message.into(),
transient,
source: Some(source.into()),
}
}
#[must_use]
pub fn is_transient(&self) -> bool {
self.transient
}
}
#[async_trait::async_trait]
pub trait EndhostApiSource: Send + Sync + 'static {
async fn endhost_apis(&self) -> Result<Vec<EndhostApiGroup>, EndhostApiSourceError>;
}
pub struct StaticEndhostApiDiscovery {
discovery_apis: Vec<Url>,
}
impl StaticEndhostApiDiscovery {
const GLOBAL_DISCOVERY_APIS: &[&'static str] = &["https://discovery.scion.anapaya.net"];
#[must_use]
pub fn new(discovery_apis: Vec<Url>) -> Self {
Self { discovery_apis }
}
#[must_use]
pub fn global() -> Self {
let discovery_apis = Self::GLOBAL_DISCOVERY_APIS
.iter()
.map(|url_str| Url::parse(url_str).expect("Invalid URL in GLOBAL_DISCOVERY_APIS"))
.collect();
Self { discovery_apis }
}
}
#[async_trait::async_trait]
impl EndhostApiSource for StaticEndhostApiDiscovery {
async fn endhost_apis(&self) -> Result<Vec<EndhostApiGroup>, EndhostApiSourceError> {
if self.discovery_apis.is_empty() {
return Err(EndhostApiSourceError::new(
"no endhost API discovery APIs configured in StaticEndhostApiDiscovery",
false,
));
}
discover_endhost_apis(self.discovery_apis.clone(), None).await
}
}
#[derive(Default)]
pub struct StaticEndhostApis {
groups: Vec<EndhostApiGroup>,
}
impl StaticEndhostApis {
#[must_use]
pub fn new() -> Self {
Self { groups: Vec::new() }
}
#[must_use]
pub fn add_group(mut self, group: Vec<Url>) -> Self {
self.groups.push(EndhostApiGroup {
apis: group
.into_iter()
.map(|url| EndhostApiInfo { address: url })
.collect(),
});
self
}
}
#[async_trait::async_trait]
impl EndhostApiSource for StaticEndhostApis {
async fn endhost_apis(&self) -> Result<Vec<EndhostApiGroup>, EndhostApiSourceError> {
Ok(self.groups.clone())
}
}
async fn discover_endhost_apis(
discovery_apis: Vec<Url>,
token_source: Option<Arc<dyn TokenSource>>,
) -> Result<Vec<EndhostApiGroup>, EndhostApiSourceError> {
let mut last_error = None;
for discovery_api in &discovery_apis {
let client = {
let mut client = match CrpcEndhostApiDiscoveryClient::new(discovery_api) {
Ok(client) => client,
Err(e) => {
tracing::warn!(%discovery_api, error = ?e, "Failed to create Endhost API discovery client");
last_error = Some(EndhostApiSourceError::with_source(
format!(
"failed to create endhost API discovery client for {discovery_api}"
),
false,
e,
));
continue;
}
};
if let Some(token_source) = token_source.clone() {
client.use_token_source(token_source);
}
client
};
match client.discover_endhost_apis().await {
Ok(discovered_apis) => {
tracing::debug!(%discovery_api, "Successfully discovered Endhost APIs");
return Ok(discovered_apis);
}
Err(e) => {
tracing::warn!(%discovery_api, error = ?e, "Failed to discover Endhost APIs");
last_error = Some(EndhostApiSourceError::with_source(
format!("failed to discover endhost APIs via {discovery_api}"),
true,
e,
));
}
}
}
match last_error {
Some(e) => {
let transient = e.is_transient();
Err(EndhostApiSourceError::with_source(
"failed to discover endhost APIs using any configured discovery API",
transient,
e,
))
}
None => {
Err(EndhostApiSourceError::new(
"attempted to discover endhost APIs with empty list of discovery APIs",
false,
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn endhost_api_source_error_transient_classification() {
assert!(EndhostApiSourceError::new("boom", true).is_transient());
assert!(!EndhostApiSourceError::new("boom", false).is_transient());
let src = || std::io::Error::other("cause");
assert!(EndhostApiSourceError::with_source("boom", true, src()).is_transient());
assert!(!EndhostApiSourceError::with_source("boom", false, src()).is_transient());
}
}