1use std::future::Future;
35
36use crate::control::ControlPlane;
37use crate::error::Error;
38use crate::region::{MICROVM_REGIONS, Region};
39
40#[derive(Clone, Debug, PartialEq)]
42pub struct Check {
43 pub name: &'static str,
44 pub ok: bool,
45 pub fatal: bool,
47 pub ran: bool,
49 pub detail: String,
50 pub remedy: String,
52}
53
54impl Check {
55 pub fn pass(name: &'static str, detail: impl Into<String>) -> Self {
56 Self {
57 name,
58 ok: true,
59 fatal: true,
60 ran: true,
61 detail: detail.into(),
62 remedy: String::new(),
63 }
64 }
65
66 pub fn fail(name: &'static str, detail: impl Into<String>, remedy: impl Into<String>) -> Self {
67 Self {
68 name,
69 ok: false,
70 fatal: true,
71 ran: true,
72 detail: detail.into(),
73 remedy: remedy.into(),
74 }
75 }
76
77 pub fn not_run(name: &'static str, reason: impl Into<String>) -> Self {
79 Self {
80 ran: false,
81 ..Self::fail(
82 name,
83 format!("not run: {}", reason.into()),
84 "fix the check above",
85 )
86 }
87 }
88
89 #[must_use]
91 pub fn advisory(mut self) -> Self {
92 self.fatal = false;
93 self
94 }
95}
96
97pub fn healthy(checks: &[Check]) -> bool {
99 checks
100 .iter()
101 .filter(|check| check.fatal)
102 .all(|check| check.ok)
103}
104
105#[derive(Clone, Debug)]
107pub struct PreflightReport {
108 pub region: Option<Region>,
110 pub checks: Vec<Check>,
112}
113
114impl PreflightReport {
115 pub fn ok(&self) -> bool {
117 healthy(&self.checks)
118 }
119
120 pub fn check(&self, name: &str) -> Option<&Check> {
122 self.checks.iter().find(|check| check.name == name)
123 }
124}
125
126fn known_regions() -> String {
127 MICROVM_REGIONS
128 .iter()
129 .map(Region::as_str)
130 .collect::<Vec<_>>()
131 .join(", ")
132}
133
134pub fn region_check(resolved: &Result<Region, Error>) -> Check {
138 match resolved {
139 Ok(region) if region.is_supported() => {
140 Check::pass("region", format!("{region} is a known MicroVMs region"))
141 }
142 Ok(region) => Check::fail(
143 "region",
144 format!("{region} is not in this client's list of MicroVMs regions"),
145 format!(
146 "known: {}; the service check below says whether it runs MicroVMs",
147 known_regions()
148 ),
149 )
150 .advisory(),
151 Err(error) => Check::fail(
152 "region",
153 error.to_string(),
154 format!("known: {}", known_regions()),
155 ),
156 }
157}
158
159pub async fn credentials_check(plane: &Result<ControlPlane, Error>, region: &Region) -> Check {
161 let failure = match plane {
162 Ok(plane) => plane
163 .resolve_credentials()
164 .await
165 .err()
166 .map(|e| e.to_string()),
167 Err(error) => Some(error.to_string()),
168 };
169 match failure {
170 None => Check::pass(
171 "credentials",
172 format!("the default chain resolved credentials for {region}"),
173 ),
174 Some(detail) => Check::fail(
175 "credentials",
176 detail,
177 "`aws sso login`, or set AWS_PROFILE / AWS_ACCESS_KEY_ID",
178 ),
179 }
180}
181
182pub async fn service_check(plane: &ControlPlane) -> Check {
184 let region = plane.region();
185 match plane.answers_listing().await {
186 Ok(bases) => Check::pass(
187 "service",
188 format!(
189 "ListManagedMicrovmImages answered in {region} ({bases} managed base(s)); API \
190 version {}",
191 crate::constants::MODEL_API_VERSION
192 ),
193 ),
194 Err(error) => Check::fail(
195 "service",
196 format!("ListManagedMicrovmImages in {region} failed: {error}"),
197 "an AccessDeniedException with no message is also what a region without MicroVMs \
198 answers; otherwise allow lambda-microvms:ListManagedMicrovmImages",
199 ),
200 }
201}
202
203pub async fn preflight(region: Option<Region>) -> PreflightReport {
208 let resolved = match region {
209 Some(region) => Ok(region),
210 None => Region::from_env(&|name| std::env::var(name).ok()),
211 };
212 preflight_with(resolved, ControlPlane::new).await
213}
214
215pub async fn preflight_with<F, Fut>(resolved: Result<Region, Error>, connect: F) -> PreflightReport
218where
219 F: FnOnce(Region) -> Fut,
220 Fut: Future<Output = Result<ControlPlane, Error>>,
221{
222 let region_line = region_check(&resolved);
223 let Ok(region) = resolved else {
224 return PreflightReport {
225 region: None,
226 checks: vec![
227 region_line,
228 Check::not_run("credentials", "no region resolved"),
229 Check::not_run("service", "no region resolved"),
230 ],
231 };
232 };
233 let plane = connect(region.clone()).await;
234 let credentials = credentials_check(&plane, ®ion).await;
235 let service = match (&plane, credentials.ok) {
236 (Ok(plane), true) => service_check(plane).await,
237 _ => Check::not_run("service", "credentials did not resolve"),
238 };
239 PreflightReport {
240 region: Some(region),
241 checks: vec![region_line, credentials, service],
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use std::sync::{Arc, Mutex};
248
249 use super::*;
250 use crate::control::fake::{Answer, FakeControlPlane, TestClock};
251 use crate::control::transport::{Call, Reply, Transport};
252 use crate::error::ErrorKind;
253
254 struct NoCredentials(Mutex<Vec<String>>);
256
257 impl Transport for NoCredentials {
258 fn send(
259 &self,
260 call: Call,
261 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Reply, Error>> + Send + '_>> {
262 self.0
263 .lock()
264 .expect("not poisoned")
265 .push(call.operation.into());
266 Box::pin(async { panic!("no call may follow a failed credential chain") })
267 }
268
269 fn resolve_credentials(
270 &self,
271 ) -> std::pin::Pin<Box<dyn Future<Output = Result<(), Error>> + Send + '_>> {
272 Box::pin(async { Err(Error::new(ErrorKind::Credentials, "nothing in the chain")) })
273 }
274 }
275
276 fn plane(transport: Arc<dyn Transport>, region: Region) -> ControlPlane {
277 ControlPlane::with_transport(transport, region, Arc::new(TestClock::new()))
278 }
279
280 #[tokio::test]
283 async fn the_outcome_is_the_models_table() {
284 for (region, listing, ok) in [
285 (Region::UsEast1, Answer::ok(r#"{"items": []}"#), true),
286 (
287 Region::unlisted("ap-south-1"),
288 Answer::ok(r#"{"items": []}"#),
289 true,
290 ),
291 (
292 Region::unlisted("ap-south-1"),
293 Answer::failure(403, "AccessDeniedException"),
294 false,
295 ),
296 ] {
297 let recorder = Arc::new(FakeControlPlane::new());
298 recorder.answer("ListManagedMicrovmImages", listing);
299 let transport = Arc::clone(&recorder) as Arc<dyn Transport>;
300 let report = preflight_with(
301 Ok(region.clone()),
302 |r| async move { Ok(plane(transport, r)) },
303 )
304 .await;
305 assert_eq!(report.ok(), ok, "{region}: {report:#?}");
306 assert_eq!(
307 recorder.operations(),
308 ["ListManagedMicrovmImages"],
309 "BIND-16: one free read"
310 );
311 assert_eq!(report.checks.len(), 3);
312 }
313 }
314
315 #[tokio::test]
321 async fn failed_credentials_stop_the_preflight_before_any_call() {
322 let transport = Arc::new(NoCredentials(Mutex::new(Vec::new())));
323 let shared = Arc::clone(&transport) as Arc<dyn Transport>;
324 let report =
325 preflight_with(Ok(Region::UsEast1), |r| async move { Ok(plane(shared, r)) }).await;
326 assert!(!report.ok());
327 let credentials = report.check("credentials").expect("reported");
328 assert!(!credentials.ok && credentials.ran && credentials.fatal);
329 let service = report.check("service").expect("reported");
330 assert!(!service.ok && !service.ran && service.fatal, "{service:?}");
331 assert!(transport.0.lock().expect("not poisoned").is_empty());
332 }
333
334 #[tokio::test]
336 async fn an_unresolved_region_builds_no_control_plane() {
337 let refused =
338 Region::from_env(&|name| (name == "AWS_REGION").then(|| "eu-central-1".into()));
339 let report = preflight_with(refused, |_| async {
340 panic!("no control plane may be built without a region")
341 })
342 .await;
343 assert!(!report.ok());
344 assert_eq!(report.region, None);
345 assert!(
346 report
347 .check("region")
348 .is_some_and(|check| check.fatal && !check.ok)
349 );
350 for name in ["credentials", "service"] {
351 assert!(report.check(name).is_some_and(|check| !check.ran), "{name}");
352 }
353 }
354
355 #[tokio::test]
358 async fn a_control_plane_that_fails_to_build_is_a_credentials_failure() {
359 let report = preflight_with(Ok(Region::UsEast1), |_| async {
360 Err(Error::new(ErrorKind::Credentials, "no provider"))
361 })
362 .await;
363 assert!(!report.ok());
364 assert!(
365 report
366 .check("credentials")
367 .is_some_and(|check| check.ran && !check.ok)
368 );
369 assert!(report.check("service").is_some_and(|check| !check.ran));
370 }
371
372 #[test]
374 fn the_region_line_is_pass_advisory_or_fatal() {
375 assert!(region_check(&Ok(Region::UsEast1)).ok);
376 let unlisted = region_check(&Ok(Region::unlisted("ap-south-1")));
377 assert!(!unlisted.ok && !unlisted.fatal);
378 let refused = region_check(&Err(Error::invalid_arg("nope")));
379 assert!(!refused.ok && refused.fatal);
380 }
381}