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}