use std::future::Future;
pub use microvms_domain::preflight::*;
use crate::control::ControlPlane;
use crate::error::Error;
use crate::region::Region;
pub async fn credentials_check(plane: &Result<ControlPlane, Error>, region: &Region) -> Check {
let failure = match plane {
Ok(plane) => plane
.resolve_credentials()
.await
.err()
.map(|e| e.to_string()),
Err(error) => Some(error.to_string()),
};
match failure {
None => Check::pass(
"credentials",
format!("the default chain resolved credentials for {region}"),
),
Some(detail) => Check::fail(
"credentials",
detail,
"`aws sso login`, or set AWS_PROFILE / AWS_ACCESS_KEY_ID",
),
}
}
pub async fn service_check(plane: &ControlPlane) -> Check {
let region = plane.region();
match plane.answers_listing().await {
Ok(bases) => Check::pass(
"service",
format!(
"ListManagedMicrovmImages answered in {region} ({bases} managed base(s)); API \
version {}",
crate::constants::MODEL_API_VERSION
),
),
Err(error) => Check::fail(
"service",
format!("ListManagedMicrovmImages in {region} failed: {error}"),
"an AccessDeniedException with no message is also what a region without MicroVMs \
answers; otherwise allow lambda:ListManagedMicrovmImages",
),
}
}
pub async fn preflight_with<F, Fut>(resolved: Result<Region, Error>, connect: F) -> PreflightReport
where
F: FnOnce(Region) -> Fut,
Fut: Future<Output = Result<ControlPlane, Error>>,
{
let region_line = region_check(&resolved);
let Ok(region) = resolved else {
return PreflightReport {
region: None,
checks: vec![
region_line,
Check::not_run("credentials", "no region resolved"),
Check::not_run("service", "no region resolved"),
],
};
};
let plane = connect(region.clone()).await;
let credentials = credentials_check(&plane, ®ion).await;
let service = match (&plane, credentials.ok) {
(Ok(plane), true) => service_check(plane).await,
_ => Check::not_run("service", "credentials did not resolve"),
};
PreflightReport {
region: Some(region),
checks: vec![region_line, credentials, service],
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use super::*;
use crate::control::fake::{Answer, FakeControlPlane, TestClock};
use crate::control::transport::{Call, Reply, Transport};
use crate::error::ErrorKind;
struct NoCredentials(Mutex<Vec<String>>);
impl Transport for NoCredentials {
fn send(
&self,
call: Call,
) -> std::pin::Pin<Box<dyn Future<Output = Result<Reply, Error>> + Send + '_>> {
self.0
.lock()
.expect("not poisoned")
.push(call.operation.into());
Box::pin(async { panic!("no call may follow a failed credential chain") })
}
fn resolve_credentials(
&self,
) -> std::pin::Pin<Box<dyn Future<Output = Result<(), Error>> + Send + '_>> {
Box::pin(async { Err(Error::new(ErrorKind::Credentials, "nothing in the chain")) })
}
}
fn plane(transport: Arc<dyn Transport>, region: Region) -> ControlPlane {
crate::testing::control_plane(transport, region, Arc::new(TestClock::new()))
}
#[tokio::test]
async fn the_outcome_is_the_models_table() {
for (region, listing, ok) in [
(Region::US_EAST_1, Answer::ok(r#"{"items": []}"#), true),
(
"us-west-1".parse().expect("a region name"),
Answer::ok(r#"{"items": []}"#),
true,
),
(
"us-west-1".parse().expect("a region name"),
Answer::failure(403, "AccessDeniedException"),
false,
),
] {
let recorder = Arc::new(FakeControlPlane::new());
recorder.answer("ListManagedMicrovmImages", listing);
let transport = Arc::clone(&recorder) as Arc<dyn Transport>;
let report = preflight_with(
Ok(region.clone()),
|r| async move { Ok(plane(transport, r)) },
)
.await;
assert_eq!(report.ok(), ok, "{region}: {report:#?}");
assert_eq!(
recorder.operations(),
["ListManagedMicrovmImages"],
"BIND-16: one free read"
);
assert_eq!(report.checks.len(), 3);
}
}
#[tokio::test]
async fn service_failure_names_the_iam_action_namespace() {
let recorder = Arc::new(FakeControlPlane::new());
recorder.answer(
"ListManagedMicrovmImages",
Answer::failure(403, "AccessDeniedException"),
);
let check = service_check(&plane(recorder, Region::US_EAST_1)).await;
assert!(!check.ok && check.ran && check.fatal);
assert!(
check
.remedy
.contains("allow lambda:ListManagedMicrovmImages")
);
assert!(!check.remedy.contains("lambda-microvms:"));
}
#[tokio::test]
async fn failed_credentials_stop_the_preflight_before_any_call() {
let transport = Arc::new(NoCredentials(Mutex::new(Vec::new())));
let shared = Arc::clone(&transport) as Arc<dyn Transport>;
let report = preflight_with(
Ok(Region::US_EAST_1),
|r| async move { Ok(plane(shared, r)) },
)
.await;
assert!(!report.ok());
let credentials = report.check("credentials").expect("reported");
assert!(!credentials.ok && credentials.ran && credentials.fatal);
let service = report.check("service").expect("reported");
assert!(!service.ok && !service.ran && service.fatal, "{service:?}");
assert!(transport.0.lock().expect("not poisoned").is_empty());
}
#[tokio::test]
async fn an_unresolved_region_builds_no_control_plane() {
let refused =
Region::from_env(&|name| (name == "AWS_REGION").then(|| "EU-CENTRAL-1".into()));
let report = preflight_with(refused, |_| async {
panic!("no control plane may be built without a region")
})
.await;
assert!(!report.ok());
assert_eq!(report.region, None);
assert!(
report
.check("region")
.is_some_and(|check| check.fatal && !check.ok)
);
for name in ["credentials", "service"] {
assert!(report.check(name).is_some_and(|check| !check.ran), "{name}");
}
}
#[tokio::test]
async fn a_control_plane_that_fails_to_build_is_a_credentials_failure() {
let report = preflight_with(Ok(Region::US_EAST_1), |_| async {
Err(Error::new(ErrorKind::Credentials, "no provider"))
})
.await;
assert!(!report.ok());
assert!(
report
.check("credentials")
.is_some_and(|check| check.ran && !check.ok)
);
assert!(report.check("service").is_some_and(|check| !check.ran));
}
#[test]
fn the_region_line_is_pass_advisory_or_fatal() {
assert!(region_check(&Ok(Region::US_EAST_1)).ok);
let unlisted = region_check(&Ok("us-west-1".parse().expect("a region name")));
assert!(!unlisted.ok && !unlisted.fatal);
let refused = region_check(&Err(Error::invalid_arg("nope")));
assert!(!refused.ok && refused.fatal);
}
}