Skip to main content

microvms_app/
preflight.rs

1// SPDX-License-Identifier: Apache-2.0
2//! `preflight`: whether a harness can launch here, checked before it queues work (#223).
3//!
4//! Three checks, in order, each a [`Check`] the CLI's `doctor` renders too (it shares
5//! [`region_check`] and [`credentials_check`] and prints the same lines):
6//!
7//! 1. **region**: the region resolves, from the caller or from `$AWS_REGION` /
8//!    `$AWS_DEFAULT_REGION` ([`Region::from_env`]). Fatal when it does not resolve. Advisory
9//!    when it is outside [`crate::region::MEASURED_REGIONS`]: AWS adds regions faster than a
10//!    constant is re-read.
11//! 2. **credentials**: the default credential chain resolves credentials
12//!    ([`ControlPlane::resolve_credentials`]). No AWS API call; the chain may read an SSO
13//!    cache, run a credential process, or ask the instance metadata service.
14//! 3. **service**: one `ListManagedMicrovmImages` page in that region
15//!    ([`ControlPlane::answers_listing`]): the endpoint resolves, the signature and the IAM
16//!    permission are accepted, and the region answers the listing. A region without
17//!    MicroVMs can answer `AccessDeniedException` with a null message (TRAP-6). A pass does
18//!    not prove the region runs MicroVMs: measured 2026-10-24, `ca-central-1`, which AWS
19//!    lists as planned, answered the listing.
20//!
21//! A check whose precondition failed is reported as not run ([`Check::ran`] false), fatal, and
22//! makes no call. [`PreflightReport::ok`] is true exactly when no fatal check failed or was
23//! skipped. The Stateright model in `crates/model/src/preflight.rs` specifies this (BIND-15, BIND-16).
24//!
25//! # Nothing billable, and what is not checked
26//!
27//! The one AWS operation is a free read (`send_with_retry` may repeat it on a throttle); no
28//! call creates, changes, or bills anything. The Python harvester's check that boto3 knows the
29//! `lambda-microvms` service has no counterpart: this client is Rust and speaks the service
30//! model it was built against ([`crate::constants::MODEL_API_VERSION`]), so the listing is the
31//! equivalent evidence that the endpoint accepts that API version. A harness that also calls
32//! the service through boto3 checks its own boto3. Preflight does not check the execution or
33//! build role, the S3 artifact bucket, image quotas, or VPC connectors: each needs a call this
34//! module does not make, and `doctor` reports the infrastructure it can see.
35
36use std::future::Future;
37
38pub use microvms_domain::preflight::*;
39
40use crate::control::ControlPlane;
41use crate::error::Error;
42use crate::region::Region;
43
44/// The credentials line, from a control plane that may not have been built.
45pub async fn credentials_check(plane: &Result<ControlPlane, Error>, region: &Region) -> Check {
46    let failure = match plane {
47        Ok(plane) => plane
48            .resolve_credentials()
49            .await
50            .err()
51            .map(|e| e.to_string()),
52        Err(error) => Some(error.to_string()),
53    };
54    match failure {
55        None => Check::pass(
56            "credentials",
57            format!("the default chain resolved credentials for {region}"),
58        ),
59        Some(detail) => Check::fail(
60            "credentials",
61            detail,
62            "`aws sso login`, or set AWS_PROFILE / AWS_ACCESS_KEY_ID",
63        ),
64    }
65}
66
67/// The service line: one free, read-only listing page.
68pub async fn service_check(plane: &ControlPlane) -> Check {
69    let region = plane.region();
70    match plane.answers_listing().await {
71        Ok(bases) => Check::pass(
72            "service",
73            format!(
74                "ListManagedMicrovmImages answered in {region} ({bases} managed base(s)); API \
75                 version {}",
76                crate::constants::MODEL_API_VERSION
77            ),
78        ),
79        Err(error) => Check::fail(
80            "service",
81            format!("ListManagedMicrovmImages in {region} failed: {error}"),
82            "an AccessDeniedException with no message is also what a region without MicroVMs \
83             answers; otherwise allow lambda:ListManagedMicrovmImages",
84        ),
85    }
86}
87
88/// `preflight` (`microvms_core::preflight::preflight`) over a resolved region and a control-plane constructor, for a caller (and a
89/// test) that supplies the transport. `connect` is not called when no region resolved.
90pub async fn preflight_with<F, Fut>(resolved: Result<Region, Error>, connect: F) -> PreflightReport
91where
92    F: FnOnce(Region) -> Fut,
93    Fut: Future<Output = Result<ControlPlane, Error>>,
94{
95    let region_line = region_check(&resolved);
96    let Ok(region) = resolved else {
97        return PreflightReport {
98            region: None,
99            checks: vec![
100                region_line,
101                Check::not_run("credentials", "no region resolved"),
102                Check::not_run("service", "no region resolved"),
103            ],
104        };
105    };
106    let plane = connect(region.clone()).await;
107    let credentials = credentials_check(&plane, &region).await;
108    let service = match (&plane, credentials.ok) {
109        (Ok(plane), true) => service_check(plane).await,
110        _ => Check::not_run("service", "credentials did not resolve"),
111    };
112    PreflightReport {
113        region: Some(region),
114        checks: vec![region_line, credentials, service],
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use std::sync::{Arc, Mutex};
121
122    use super::*;
123    use crate::control::fake::{Answer, FakeControlPlane, TestClock};
124    use crate::control::transport::{Call, Reply, Transport};
125    use crate::error::ErrorKind;
126
127    /// A recorder whose credential chain fails.
128    struct NoCredentials(Mutex<Vec<String>>);
129
130    impl Transport for NoCredentials {
131        fn send(
132            &self,
133            call: Call,
134        ) -> std::pin::Pin<Box<dyn Future<Output = Result<Reply, Error>> + Send + '_>> {
135            self.0
136                .lock()
137                .expect("not poisoned")
138                .push(call.operation.into());
139            Box::pin(async { panic!("no call may follow a failed credential chain") })
140        }
141
142        fn resolve_credentials(
143            &self,
144        ) -> std::pin::Pin<Box<dyn Future<Output = Result<(), Error>> + Send + '_>> {
145            Box::pin(async { Err(Error::new(ErrorKind::Credentials, "nothing in the chain")) })
146        }
147    }
148
149    fn plane(transport: Arc<dyn Transport>, region: Region) -> ControlPlane {
150        crate::testing::control_plane(transport, region, Arc::new(TestClock::new()))
151    }
152
153    /// **BIND-15, the model's table**: `ok` is true exactly when no line blocks, and an
154    /// unlisted region's advisory line does not.
155    #[tokio::test]
156    async fn the_outcome_is_the_models_table() {
157        for (region, listing, ok) in [
158            (Region::US_EAST_1, Answer::ok(r#"{"items": []}"#), true),
159            (
160                "us-west-1".parse().expect("a region name"),
161                Answer::ok(r#"{"items": []}"#),
162                true,
163            ),
164            (
165                "us-west-1".parse().expect("a region name"),
166                Answer::failure(403, "AccessDeniedException"),
167                false,
168            ),
169        ] {
170            let recorder = Arc::new(FakeControlPlane::new());
171            recorder.answer("ListManagedMicrovmImages", listing);
172            let transport = Arc::clone(&recorder) as Arc<dyn Transport>;
173            let report = preflight_with(
174                Ok(region.clone()),
175                |r| async move { Ok(plane(transport, r)) },
176            )
177            .await;
178            assert_eq!(report.ok(), ok, "{region}: {report:#?}");
179            assert_eq!(
180                recorder.operations(),
181                ["ListManagedMicrovmImages"],
182                "BIND-16: one free read"
183            );
184            assert_eq!(report.checks.len(), 3);
185        }
186    }
187
188    #[tokio::test]
189    async fn service_failure_names_the_iam_action_namespace() {
190        let recorder = Arc::new(FakeControlPlane::new());
191        recorder.answer(
192            "ListManagedMicrovmImages",
193            Answer::failure(403, "AccessDeniedException"),
194        );
195        let check = service_check(&plane(recorder, Region::US_EAST_1)).await;
196        assert!(!check.ok && check.ran && check.fatal);
197        assert!(
198            check
199                .remedy
200                .contains("allow lambda:ListManagedMicrovmImages")
201        );
202        assert!(!check.remedy.contains("lambda-microvms:"));
203    }
204
205    /// **BIND-16: no call after the credentials fail**, and the service line says it did not
206    /// run rather than passing or vanishing.
207    ///
208    /// **Falsification** — 2026-09-24. Run `service_check` whatever the credentials line says
209    /// and `NoCredentials::send` panics; restored.
210    #[tokio::test]
211    async fn failed_credentials_stop_the_preflight_before_any_call() {
212        let transport = Arc::new(NoCredentials(Mutex::new(Vec::new())));
213        let shared = Arc::clone(&transport) as Arc<dyn Transport>;
214        let report = preflight_with(
215            Ok(Region::US_EAST_1),
216            |r| async move { Ok(plane(shared, r)) },
217        )
218        .await;
219        assert!(!report.ok());
220        let credentials = report.check("credentials").expect("reported");
221        assert!(!credentials.ok && credentials.ran && credentials.fatal);
222        let service = report.check("service").expect("reported");
223        assert!(!service.ok && !service.ran && service.fatal, "{service:?}");
224        assert!(transport.0.lock().expect("not poisoned").is_empty());
225    }
226
227    /// **BIND-16: no region, no control plane, no call.**
228    #[tokio::test]
229    async fn an_unresolved_region_builds_no_control_plane() {
230        let refused =
231            Region::from_env(&|name| (name == "AWS_REGION").then(|| "EU-CENTRAL-1".into()));
232        let report = preflight_with(refused, |_| async {
233            panic!("no control plane may be built without a region")
234        })
235        .await;
236        assert!(!report.ok());
237        assert_eq!(report.region, None);
238        assert!(
239            report
240                .check("region")
241                .is_some_and(|check| check.fatal && !check.ok)
242        );
243        for name in ["credentials", "service"] {
244            assert!(report.check(name).is_some_and(|check| !check.ran), "{name}");
245        }
246    }
247
248    /// A control plane that could not be built is a failed credentials line, and the service
249    /// is not asked.
250    #[tokio::test]
251    async fn a_control_plane_that_fails_to_build_is_a_credentials_failure() {
252        let report = preflight_with(Ok(Region::US_EAST_1), |_| async {
253            Err(Error::new(ErrorKind::Credentials, "no provider"))
254        })
255        .await;
256        assert!(!report.ok());
257        assert!(
258            report
259                .check("credentials")
260                .is_some_and(|check| check.ran && !check.ok)
261        );
262        assert!(report.check("service").is_some_and(|check| !check.ran));
263    }
264
265    /// The region line's three cases, which `doctor` shares.
266    #[test]
267    fn the_region_line_is_pass_advisory_or_fatal() {
268        assert!(region_check(&Ok(Region::US_EAST_1)).ok);
269        let unlisted = region_check(&Ok("us-west-1".parse().expect("a region name")));
270        assert!(!unlisted.ok && !unlisted.fatal);
271        let refused = region_check(&Err(Error::invalid_arg("nope")));
272        assert!(!refused.ok && refused.fatal);
273    }
274}