Skip to main content

microvms_domain/
preflight.rs

1// SPDX-License-Identifier: Apache-2.0
2//! The lines of a `preflight` or `doctor` report, and the region line's rule (#223).
3//!
4//! The checks that call something (credentials, the service listing) and `preflight` itself
5//! are in `microvms_core::preflight`, which also documents the outcome rules: a check whose
6//! precondition failed is reported as not run ([`Check::ran`] false), fatal, and makes no
7//! call, and [`PreflightReport::ok`] is true exactly when no fatal check failed or was
8//! skipped. The Stateright model in `crates/model/src/preflight.rs` specifies this (BIND-15,
9//! BIND-16).
10
11use crate::error::Error;
12use crate::region::{MICROVM_REGIONS, Region};
13
14/// One line of a preflight or `doctor` report.
15#[derive(Clone, Debug, PartialEq)]
16pub struct Check {
17    pub name: &'static str,
18    pub ok: bool,
19    /// Whether a failure decides the outcome. An advisory failure is reported and ignored.
20    pub fatal: bool,
21    /// False when an earlier check's failure kept this one from running.
22    pub ran: bool,
23    pub detail: String,
24    /// What to do about a failure; empty on a pass.
25    pub remedy: String,
26}
27
28impl Check {
29    pub fn pass(name: &'static str, detail: impl Into<String>) -> Self {
30        Self {
31            name,
32            ok: true,
33            fatal: true,
34            ran: true,
35            detail: detail.into(),
36            remedy: String::new(),
37        }
38    }
39
40    pub fn fail(name: &'static str, detail: impl Into<String>, remedy: impl Into<String>) -> Self {
41        Self {
42            name,
43            ok: false,
44            fatal: true,
45            ran: true,
46            detail: detail.into(),
47            remedy: remedy.into(),
48        }
49    }
50
51    /// A check that did not run because `reason`; it fails, fatally, and made no call.
52    pub fn not_run(name: &'static str, reason: impl Into<String>) -> Self {
53        Self {
54            ran: false,
55            ..Self::fail(
56                name,
57                format!("not run: {}", reason.into()),
58                "fix the check above",
59            )
60        }
61    }
62
63    /// A non-fatal finding: reported, but it does not decide the outcome.
64    #[must_use]
65    pub fn advisory(mut self) -> Self {
66        self.fatal = false;
67        self
68    }
69}
70
71/// Whether every fatal check passed.
72pub fn healthy(checks: &[Check]) -> bool {
73    checks
74        .iter()
75        .filter(|check| check.fatal)
76        .all(|check| check.ok)
77}
78
79/// What `microvms_core::preflight::preflight` found.
80#[derive(Clone, Debug)]
81pub struct PreflightReport {
82    /// The region checked, or `None` when none resolved.
83    pub region: Option<Region>,
84    /// `region`, `credentials`, and `service`, in that order, always all three.
85    pub checks: Vec<Check>,
86}
87
88impl PreflightReport {
89    /// True exactly when no fatal check failed or was skipped: a launch could proceed.
90    pub fn ok(&self) -> bool {
91        healthy(&self.checks)
92    }
93
94    /// The check named `name`, if the report has one.
95    pub fn check(&self, name: &str) -> Option<&Check> {
96        self.checks.iter().find(|check| check.name == name)
97    }
98}
99
100fn known_regions() -> String {
101    MICROVM_REGIONS
102        .iter()
103        .map(Region::as_str)
104        .collect::<Vec<_>>()
105        .join(", ")
106}
107
108/// The region line: pass for a supported region, advisory for an unlisted one, fatal when
109/// none resolved. `doctor` marks the last advisory too, because it must run in a broken
110/// environment and report everything else.
111pub fn region_check(resolved: &Result<Region, Error>) -> Check {
112    match resolved {
113        Ok(region) if region.is_supported() => {
114            Check::pass("region", format!("{region} is a known MicroVMs region"))
115        }
116        Ok(region) => Check::fail(
117            "region",
118            format!("{region} is not in this client's list of MicroVMs regions"),
119            format!(
120                "known: {}; the service check below says whether it runs MicroVMs",
121                known_regions()
122            ),
123        )
124        .advisory(),
125        Err(error) => Check::fail(
126            "region",
127            error.to_string(),
128            format!("known: {}", known_regions()),
129        ),
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn only_a_fatal_failure_is_unhealthy() {
139        let advisory = Check::fail("region", "unlisted", "see below").advisory();
140        assert!(healthy(&[Check::pass("a", "fine"), advisory.clone()]));
141        assert!(!healthy(&[
142            advisory,
143            Check::not_run("service", "no region")
144        ]));
145        assert!(!Check::not_run("service", "no region").ran);
146    }
147
148    #[test]
149    fn the_region_line_passes_a_listed_region_and_advises_on_an_unlisted_one() {
150        assert!(region_check(&Ok(Region::UsEast1)).ok);
151        let unlisted = region_check(&Ok(Region::unlisted("xx-new-1")));
152        assert!(!unlisted.ok && !unlisted.fatal, "{unlisted:?}");
153        let refused = region_check(&Err(Error::invalid_arg("no region")));
154        assert!(!refused.ok && refused.fatal, "{refused:?}");
155        assert!(refused.remedy.contains("us-east-1"), "{refused:?}");
156    }
157}