Skip to main content

microvms_app/
adapters.rs

1// SPDX-License-Identifier: Apache-2.0
2//! The adapters port: the production pieces a use case builds for itself partway through.
3//!
4//! # Why a port for construction
5//!
6//! Most ports are handed to a use case whole: a [`ControlPlane`](crate::control::ControlPlane)
7//! gets its transport, clock and entropy when it's built. Two pieces can't be, because what
8//! they're built for isn't known until later. A sandbox learns its VM's endpoint from the
9//! launch reply, and only then can it build the HTTP backend its session talks through, and
10//! `ensure_image` builds its STS and S3 client on first use, for the plane's region. Building
11//! either inline would put reqwest and the credential chain inside the use case, so the use
12//! case asks an [`Adapters`] for them instead.
13//!
14//! `SystemAdapters` (in `microvms-edges`) is the production implementation. A test passes one that hands out
15//! fakes, or that refuses, so a session a sandbox builds can't dial a real endpoint by
16//! accident.
17
18use std::fmt;
19use std::sync::Arc;
20use std::time::Duration;
21
22use futures_util::future::BoxFuture;
23
24use crate::clock::Clock;
25use crate::control::BuildServices;
26use crate::error::Error;
27use crate::region::Region;
28use crate::session::SharedBackend;
29
30/// The production pieces a use case builds once it knows what they're for.
31pub trait Adapters: Send + Sync + fmt::Debug {
32    /// The HTTP backend a session against `endpoint` sends through, whose non-streaming
33    /// requests time out after `timeout`.
34    fn http_backend(&self, endpoint: &str, timeout: Duration) -> Result<SharedBackend, Error>;
35
36    /// The STS and S3 calls `ensure_image` makes, with credentials for `region`.
37    fn build_services(
38        &self,
39        region: Region,
40    ) -> BoxFuture<'_, Result<Arc<dyn BuildServices>, Error>>;
41
42    /// The clock a session reads when its builder was given none.
43    ///
44    /// A session a sandbox builds takes the plane's clock instead, so the lifecycle and the
45    /// session it hands out read one time source.
46    fn clock(&self) -> Arc<dyn Clock>;
47
48    /// Reports a warning no caller is left to receive, such as a `Sandbox` dropped with its VM
49    /// still billing.
50    ///
51    /// A use case can't write to a stream itself, since the driving adapter owns those. It
52    /// mustn't panic or block: `Drop` calls it, and a panic there during an unwind aborts.
53    fn warn(&self, warning: &str);
54}
55
56#[cfg(any(test, feature = "test-support"))]
57#[cfg_attr(test, mutants::skip)] // a test double; see .cargo/mutants.toml
58pub(crate) mod testing {
59    //! The adapters the crate's tests wire in when nothing under test should reach a network.
60
61    use super::*;
62    use crate::clock::testing::TestClock;
63    use crate::error::{ErrorKind, WireKind};
64    use crate::session::{HttpBackend, HttpRequest, HttpResponse, OpenStream};
65
66    /// Adapters that build nothing real.
67    ///
68    /// The HTTP backend answers every request with a transport failure naming it, and the
69    /// build services refuse. A test that wants a session to succeed passes its own backend
70    /// through `Sandbox::with_session_backend` or `SessionBuilder::with_backend`, which win
71    /// over the adapters, or through [`TestAdapters::with_backend`] for a session nothing else
72    /// can hand one to (`Sandbox::adopt` builds its own). A test that sets none can't reach the
73    /// endpoint its fake launch reply named.
74    #[derive(Default)]
75    pub struct TestAdapters {
76        clock: Arc<TestClock>,
77        warnings: std::sync::Mutex<Vec<String>>,
78        backend: Option<SharedBackend>,
79    }
80
81    impl std::fmt::Debug for TestAdapters {
82        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83            f.debug_struct("TestAdapters")
84                .field("backend", &self.backend.as_ref().map(|_| "scripted"))
85                .finish_non_exhaustive()
86        }
87    }
88
89    impl TestAdapters {
90        pub fn new() -> Self {
91            Self::default()
92        }
93
94        /// Adapters whose default session clock is `clock`.
95        pub fn with_clock(clock: Arc<TestClock>) -> Self {
96            Self {
97                clock,
98                ..Self::default()
99            }
100        }
101
102        /// Adapters whose sessions all send through `backend`.
103        #[must_use]
104        pub fn with_backend(mut self, backend: SharedBackend) -> Self {
105            self.backend = Some(backend);
106            self
107        }
108
109        /// Every warning reported through these adapters, oldest first.
110        pub fn warnings(&self) -> Vec<String> {
111            self.warnings
112                .lock()
113                .unwrap_or_else(std::sync::PoisonError::into_inner)
114                .clone()
115        }
116    }
117
118    impl Adapters for TestAdapters {
119        fn http_backend(&self, endpoint: &str, _timeout: Duration) -> Result<SharedBackend, Error> {
120            Ok(match &self.backend {
121                Some(backend) => Arc::clone(backend),
122                None => Arc::new(Inert {
123                    endpoint: endpoint.to_string(),
124                }),
125            })
126        }
127
128        fn build_services(
129            &self,
130            region: Region,
131        ) -> BoxFuture<'_, Result<Arc<dyn BuildServices>, Error>> {
132            Box::pin(async move {
133                Err(Error::new(
134                    ErrorKind::Credentials,
135                    format!(
136                        "the test adapters build no STS or S3 client for {region}; pass one \
137                         with Sandbox::with_build_services"
138                    ),
139                ))
140            })
141        }
142
143        fn clock(&self) -> Arc<dyn Clock> {
144            Arc::clone(&self.clock) as Arc<dyn Clock>
145        }
146
147        fn warn(&self, warning: &str) {
148            self.warnings
149                .lock()
150                .unwrap_or_else(std::sync::PoisonError::into_inner)
151                .push(warning.to_string());
152        }
153    }
154
155    /// A backend that fails every request, naming the endpoint it wasn't allowed to reach.
156    struct Inert {
157        endpoint: String,
158    }
159
160    impl Inert {
161        fn refuse(&self, request: &HttpRequest) -> Error {
162            Error::wire(
163                WireKind::Transport,
164                format!(
165                    "{} {} was sent to {} through the test adapters, which reach nothing",
166                    request.method, request.path, self.endpoint
167                ),
168            )
169        }
170    }
171
172    impl HttpBackend for Inert {
173        fn send(&self, request: HttpRequest) -> BoxFuture<'_, Result<HttpResponse, Error>> {
174            let error = self.refuse(&request);
175            Box::pin(async move { Err(error) })
176        }
177
178        fn open_stream(
179            &self,
180            request: HttpRequest,
181            _idle_timeout: Duration,
182        ) -> BoxFuture<'_, Result<OpenStream, Error>> {
183            let error = self.refuse(&request);
184            Box::pin(async move { Err(error) })
185        }
186    }
187}