use std::future::Future;
use crate::control::ControlPlane;
use crate::error::Error;
use crate::region::{MICROVM_REGIONS, Region};
#[derive(Clone, Debug, PartialEq)]
pub struct Check {
pub name: &'static str,
pub ok: bool,
pub fatal: bool,
pub ran: bool,
pub detail: String,
pub remedy: String,
}
impl Check {
pub fn pass(name: &'static str, detail: impl Into<String>) -> Self {
Self {
name,
ok: true,
fatal: true,
ran: true,
detail: detail.into(),
remedy: String::new(),
}
}
pub fn fail(name: &'static str, detail: impl Into<String>, remedy: impl Into<String>) -> Self {
Self {
name,
ok: false,
fatal: true,
ran: true,
detail: detail.into(),
remedy: remedy.into(),
}
}
pub fn not_run(name: &'static str, reason: impl Into<String>) -> Self {
Self {
ran: false,
..Self::fail(
name,
format!("not run: {}", reason.into()),
"fix the check above",
)
}
}
#[must_use]
pub fn advisory(mut self) -> Self {
self.fatal = false;
self
}
}
pub fn healthy(checks: &[Check]) -> bool {
checks
.iter()
.filter(|check| check.fatal)
.all(|check| check.ok)
}
#[derive(Clone, Debug)]
pub struct PreflightReport {
pub region: Option<Region>,
pub checks: Vec<Check>,
}
impl PreflightReport {
pub fn ok(&self) -> bool {
healthy(&self.checks)
}
pub fn check(&self, name: &str) -> Option<&Check> {
self.checks.iter().find(|check| check.name == name)
}
}
fn known_regions() -> String {
MICROVM_REGIONS
.iter()
.map(Region::as_str)
.collect::<Vec<_>>()
.join(", ")
}
pub fn region_check(resolved: &Result<Region, Error>) -> Check {
match resolved {
Ok(region) if region.is_supported() => {
Check::pass("region", format!("{region} is a known MicroVMs region"))
}
Ok(region) => Check::fail(
"region",
format!("{region} is not in this client's list of MicroVMs regions"),
format!(
"known: {}; the service check below says whether it runs MicroVMs",
known_regions()
),
)
.advisory(),
Err(error) => Check::fail(
"region",
error.to_string(),
format!("known: {}", known_regions()),
),
}
}
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-microvms:ListManagedMicrovmImages",
),
}
}
pub async fn preflight(region: Option<Region>) -> PreflightReport {
let resolved = match region {
Some(region) => Ok(region),
None => Region::from_env(&|name| std::env::var(name).ok()),
};
preflight_with(resolved, ControlPlane::new).await
}
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 {
ControlPlane::with_transport(transport, region, Arc::new(TestClock::new()))
}
#[tokio::test]
async fn the_outcome_is_the_models_table() {
for (region, listing, ok) in [
(Region::UsEast1, Answer::ok(r#"{"items": []}"#), true),
(
Region::unlisted("ap-south-1"),
Answer::ok(r#"{"items": []}"#),
true,
),
(
Region::unlisted("ap-south-1"),
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 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::UsEast1), |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::UsEast1), |_| 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::UsEast1)).ok);
let unlisted = region_check(&Ok(Region::unlisted("ap-south-1")));
assert!(!unlisted.ok && !unlisted.fatal);
let refused = region_check(&Err(Error::invalid_arg("nope")));
assert!(!refused.ok && refused.fatal);
}
}