Skip to main content

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}