1use std::future::Future;
37
38pub use microvms_domain::preflight::*;
39
40use crate::control::ControlPlane;
41use crate::error::Error;
42use crate::region::Region;
43
44pub 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
67pub 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
88pub 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, ®ion).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 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 #[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 #[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 #[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 #[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 #[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}