scion_stack/ea_source.rs
1// Copyright 2026 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Endhost API Source.
16//!
17//! This module defines the `EndhostApiSource` trait, which is responsible for discovering and
18//! providing access to the Endhost API. Implementations of this trait can be used to discover the
19//! API in different environments, such as through environment variables, configuration files, or
20//! network discovery.
21
22use std::sync::Arc;
23
24use endhost_api_discovery_client::client::{
25 CrpcEndhostApiDiscoveryClient, EndhostApiDiscoveryClient,
26};
27use endhost_api_discovery_models::{EndhostApiGroup, EndhostApiInfo};
28use snap_control::reexport::TokenSource;
29use url::Url;
30
31/// Re-exports of Endhost API discovery models from `endhost_api_discovery_models`
32pub mod models {
33 pub use endhost_api_discovery_models::*;
34}
35
36/// Error type for failures to retrieve Endhost APIs from an `EndhostApiSource`.
37#[derive(Debug, thiserror::Error)]
38#[error("Failed to retrieve Endhost APIs: {error}")]
39pub struct EndhostApiSourceError {
40 /// The underlying error that occurred while trying to retrieve the Endhost APIs
41 pub error: anyhow::Error,
42 /// If true, the error is considered transient and the client may retry
43 pub transient: bool,
44}
45
46/// Returns available Endhost APIs for the client to use
47///
48/// Endhost APIs are grouped into `EndhostApiGroup`s.
49/// The client should attempt to use the first Group
50#[async_trait::async_trait]
51pub trait EndhostApiSource: Send + Sync + 'static {
52 /// Returns the available Endhost APIs.
53 async fn endhost_apis(&self) -> Result<Vec<EndhostApiGroup>, EndhostApiSourceError>;
54}
55
56/// A static list of Endhost API discovery services which the stack can use to discover Endhost
57/// APIs.
58pub struct StaticEndhostApiDiscovery {
59 discovery_apis: Vec<Url>,
60}
61
62impl StaticEndhostApiDiscovery {
63 const GLOBAL_DISCOVERY_APIS: &[&'static str] = &["https://discovery.scion.anapaya.net"];
64
65 /// Creates a new `StaticEndhostApiDiscovery` with the given list of discovery API URLs.
66 pub fn new(discovery_apis: Vec<Url>) -> Self {
67 Self { discovery_apis }
68 }
69
70 /// Creates a new `StaticEndhostApiDiscovery` with the global list of discovery API URLs.
71 pub fn global() -> Self {
72 let discovery_apis = Self::GLOBAL_DISCOVERY_APIS
73 .iter()
74 .map(|url_str| Url::parse(url_str).expect("Invalid URL in GLOBAL_DISCOVERY_APIS"))
75 .collect();
76
77 Self { discovery_apis }
78 }
79}
80
81#[async_trait::async_trait]
82impl EndhostApiSource for StaticEndhostApiDiscovery {
83 /// Returns the available Endhost APIs.
84 async fn endhost_apis(&self) -> Result<Vec<EndhostApiGroup>, EndhostApiSourceError> {
85 if self.discovery_apis.is_empty() {
86 return Err(EndhostApiSourceError {
87 error: anyhow::anyhow!(
88 "No Endhost API discovery APIs configured in StaticEndhostApiDiscovery"
89 ),
90 transient: false,
91 });
92 }
93
94 discover_endhost_apis(self.discovery_apis.clone(), None).await
95 }
96}
97
98/// A static list of Endhost APIs which the stack can use.
99#[derive(Default)]
100pub struct StaticEndhostApis {
101 /// List of Endhost API groups to use
102 groups: Vec<EndhostApiGroup>,
103}
104
105impl StaticEndhostApis {
106 /// Creates a new empty `StaticEndhostApis`
107 pub fn new() -> Self {
108 Self { groups: Vec::new() }
109 }
110
111 /// Adds a group of Endhost APIs to the list of available APIs.
112 ///
113 /// Endhost APIs in one group must offer the same data when queried. Meaning they should know
114 /// the same set of underlays and segments.
115 ///
116 /// The client can freely failover between APIs in the same group.
117 ///
118 /// Endhost APIs in different groups can differ in the data they offer, however the client must
119 /// close all open connections to failover between groups.
120 pub fn add_group(mut self, group: Vec<Url>) -> Self {
121 self.groups.push(EndhostApiGroup {
122 apis: group
123 .into_iter()
124 .map(|url| EndhostApiInfo { address: url })
125 .collect(),
126 });
127
128 self
129 }
130}
131
132#[async_trait::async_trait]
133impl EndhostApiSource for StaticEndhostApis {
134 /// Returns the available Endhost APIs.
135 async fn endhost_apis(&self) -> Result<Vec<EndhostApiGroup>, EndhostApiSourceError> {
136 Ok(self.groups.clone())
137 }
138}
139
140/// Attempts to discover Endhost APIs using all provided discovery API URLs, returning the first
141/// successful result or an error if all discovery APIs fail.
142///
143/// On failure, returns the last error encountered or a generic error if no discovery APIs were
144/// provided.
145async fn discover_endhost_apis(
146 discovery_apis: Vec<Url>,
147 token_source: Option<Arc<dyn TokenSource>>,
148) -> Result<Vec<EndhostApiGroup>, EndhostApiSourceError> {
149 let mut last_error = None;
150 for discovery_api in discovery_apis.iter() {
151 // Try all apis in order, return the first successful one
152 let client = {
153 let mut client = match CrpcEndhostApiDiscoveryClient::new(discovery_api) {
154 Ok(client) => client,
155 Err(e) => {
156 tracing::warn!(%discovery_api, error = ?e, "Failed to create Endhost API discovery client");
157 // Track last error so we can return it if all discovery APIs fail
158 last_error = Some(EndhostApiSourceError {
159 error: e.context(format!(
160 "Failed to create Endhost API discovery client for {}",
161 discovery_api
162 )),
163 transient: false,
164 });
165 continue;
166 }
167 };
168
169 if let Some(token_source) = token_source.clone() {
170 client.use_token_source(token_source);
171 }
172
173 client
174 };
175
176 match client.discover_endhost_apis().await {
177 Ok(discovered_apis) => {
178 tracing::debug!(%discovery_api, "Successfully discovered Endhost APIs");
179 return Ok(discovered_apis);
180 }
181 Err(e) => {
182 tracing::warn!(%discovery_api, error = ?e, "Failed to discover Endhost APIs");
183 // Track last error so we can return it if all discovery APIs fail
184 last_error = Some(EndhostApiSourceError {
185 error: anyhow::Error::new(e),
186 transient: true,
187 });
188
189 continue;
190 }
191 }
192 }
193
194 // If we exhausted all discovery APIs, return the last error we encountered or a generic
195 // error if we had no discovery APIs configured
196 match last_error {
197 Some(e) => {
198 let transient = e.transient;
199
200 Err(EndhostApiSourceError {
201 error: anyhow::Error::new(e)
202 .context("Failed to discover Endhost APIs using any configured discovery APIs"),
203 transient,
204 })
205 }
206 None => {
207 Err(EndhostApiSourceError {
208 error: anyhow::anyhow!(
209 "Attempted to discover Endhost APIs with empty list of discovery APIs"
210 ),
211 transient: false,
212 })
213 }
214 }
215}