Skip to main content

planetary_api/
lib.rs

1//! The implementation of the Planetary TES API server.
2
3use std::future::Future;
4use std::sync::Arc;
5use std::time::Duration;
6
7use axum::extract::Request;
8use axum::middleware::Next;
9use axum::response::IntoResponse;
10use axum::response::Response;
11use axum_extra::TypedHeader;
12use axum_extra::headers::Authorization;
13use axum_extra::headers::authorization::Basic;
14use bon::Builder;
15use planetary_db::Database;
16use planetary_server::DEFAULT_ADDRESS;
17use planetary_server::DEFAULT_PORT;
18use planetary_server::Error;
19use planetary_server::templating::is_username_valid;
20use reqwest::Client;
21use secrecy::SecretString;
22use tes::v1::types::responses::ServiceInfo;
23use tokio_retry2::strategy::ExponentialFactorBackoff;
24use tokio_retry2::strategy::MaxInterval;
25use tracing::warn;
26use url::Url;
27
28mod info;
29mod tasks;
30
31/// Gets an iterator over the retry durations for network operations.
32///
33/// Retries use an exponential power of 2 backoff, starting at 1 second with
34/// a maximum duration of 60 seconds.
35fn retry_durations() -> impl Iterator<Item = Duration> {
36    const INITIAL_DELAY_MILLIS: u64 = 1000;
37    const BASE_FACTOR: f64 = 2.0;
38    const MAX_DURATION: Duration = Duration::from_secs(60);
39    const RETRIES: usize = 5;
40
41    ExponentialFactorBackoff::from_millis(INITIAL_DELAY_MILLIS, BASE_FACTOR)
42        .max_duration(MAX_DURATION)
43        .take(RETRIES)
44}
45
46/// Helper for notifying that a network operation failed and will be retried.
47fn notify_retry(e: &reqwest::Error, duration: Duration) {
48    warn!(
49        "network operation failed: {e} (retrying after {duration} seconds)",
50        duration = duration.as_secs()
51    );
52}
53
54/// An extension for passing the request username through from the
55/// authentication middleware.
56#[derive(Clone)]
57struct Username(String);
58
59/// Middleware function to perform auth lookup against the request.
60///
61/// This middleware does not actually perform any authentication of the user.
62///
63/// If the `X-Forwarded-User` header is present, it is respected over the
64/// `Authorization` header.
65///
66/// The intention of this middleware is to simply to enforce that a username was
67/// sent with the request.
68async fn auth(
69    allow_authorization_fallback: bool,
70    authorization: Option<TypedHeader<Authorization<Basic>>>,
71    mut request: Request,
72    next: Next,
73) -> Response {
74    // First respect the `X-Forwarded-User` header, then the `Authorization`
75    // header
76    let username = match (
77        request
78            .headers()
79            .get("X-Forwarded-User")
80            .map(|v| v.to_str()),
81        &authorization,
82    ) {
83        // Note: if the `X-Forwarded-User` is present but malformed (i.e. `Some(Err(_))`), then we
84        // intentionally return forbidden rather than look at the `Authorization` header
85        (Some(Ok(username)), _) if !username.is_empty() => username,
86        (None, Some(auth)) if allow_authorization_fallback => auth.username(),
87        _ => return Error::forbidden().into_response(),
88    }
89    .to_string();
90
91    // Ensure the username is valid
92    if username.is_empty() || !is_username_valid(&username) {
93        return Error::bad_request("username is invalid").into_response();
94    }
95
96    request.extensions_mut().insert(Username(username));
97    next.run(request).await
98}
99
100/// Represents information about the orchestrator service.
101struct OrchestratorServiceInfo {
102    /// The URL of the orchestrator service.
103    url: Url,
104    /// The orchestrator service API key.
105    api_key: SecretString,
106}
107
108/// The state for the server.
109#[derive(Clone)]
110struct State {
111    /// The HTTP client for communicating with the orchestration service.
112    client: Arc<Client>,
113    /// The service information.
114    info: Arc<ServiceInfo>,
115    /// The TES database.
116    database: Arc<dyn Database>,
117    /// The orchestrator service information.
118    orchestrator: Arc<OrchestratorServiceInfo>,
119    /// Whether or not to allow `file://` URLs for task inputs and outputs.
120    allow_file_urls: bool,
121}
122
123impl State {
124    /// Constructs a new state given the service info, database, and
125    /// orchestrator service sender.
126    pub fn new(
127        info: ServiceInfo,
128        database: Arc<dyn Database>,
129        orchestrator_url: Url,
130        orchestrator_api_key: SecretString,
131        allow_file_urls: bool,
132    ) -> Self {
133        Self {
134            client: Arc::new(Client::new()),
135            info: Arc::new(info),
136            database: database.clone(),
137            orchestrator: Arc::new(OrchestratorServiceInfo {
138                url: orchestrator_url,
139                api_key: orchestrator_api_key,
140            }),
141            allow_file_urls,
142        }
143    }
144}
145
146/// The TES API server.
147#[derive(Clone, Builder)]
148pub struct Server {
149    /// The address to bind the server to.
150    #[builder(into, default = DEFAULT_ADDRESS)]
151    address: String,
152
153    /// The port to bind the server to.
154    #[builder(into, default = DEFAULT_PORT)]
155    port: u16,
156
157    /// The service information.
158    #[builder(into)]
159    info: ServiceInfo,
160
161    /// The TES database to use for the server.
162    #[builder(name = "shared_database")]
163    database: Arc<dyn Database>,
164
165    /// The Planetary orchestrator service URL.
166    #[builder(into)]
167    orchestrator_url: Url,
168
169    /// The Planetary orchestrator service API key.
170    #[builder(into)]
171    orchestrator_api_key: SecretString,
172
173    /// Whether or not to allow fallback to the `Authorization` header.
174    allow_authorization_fallback: bool,
175
176    /// Whether or not to allow `file://` URLs for task inputs and outputs.
177    allow_file_urls: bool,
178}
179
180impl<S: server_builder::State> ServerBuilder<S> {
181    /// The TES database to use for the server.
182    ///
183    /// This is a convenience method for setting the shared database server
184    /// from any type that implements `Database`.
185    pub fn database(
186        self,
187        database: impl Database + 'static,
188    ) -> ServerBuilder<server_builder::SetSharedDatabase<S>>
189    where
190        S::SharedDatabase: server_builder::IsUnset,
191    {
192        self.shared_database(Arc::new(database))
193    }
194}
195
196impl Server {
197    /// Runs the server.
198    pub async fn run<F>(self, shutdown: F) -> anyhow::Result<()>
199    where
200        F: Future<Output = ()> + Send + 'static,
201    {
202        let server = planetary_server::Server::builder()
203            .address(self.address)
204            .port(self.port)
205            .routers(bon::vec![
206                info::router(),
207                tasks::router(self.allow_authorization_fallback)
208            ])
209            .build();
210
211        let state = State::new(
212            self.info,
213            self.database,
214            self.orchestrator_url,
215            self.orchestrator_api_key,
216            self.allow_file_urls,
217        );
218
219        server.run(state, shutdown).await?;
220        Ok(())
221    }
222}