Skip to content

Commit 2bdc2c5

Browse files
authored
Merge pull request #1435 from lann/spin-doctor
`spin doctor` prototype
2 parents aecbb29 + 5cbe8d6 commit 2bdc2c5

22 files changed

+1196
-7
lines changed

Cargo.lock

Lines changed: 72 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ spin-bindle = { path = "crates/bindle" }
5252
spin-build = { path = "crates/build" }
5353
spin-common = { path = "crates/common" }
5454
spin-config = { path = "crates/config" }
55+
spin-doctor = { path = "crates/doctor" }
5556
spin-http = { path = "crates/http" }
5657
spin-trigger-http = { path = "crates/trigger-http" }
5758
spin-loader = { path = "crates/loader" }

crates/doctor/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "spin-doctor"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
anyhow = "1"
8+
async-trait = "0.1"
9+
serde = { version = "1", features = ["derive"] }
10+
similar = "2"
11+
spin-loader = { path = "../loader" }
12+
tokio = "1"
13+
toml = "0.7"
14+
toml_edit = "0.19"
15+
tracing = { workspace = true }
16+
17+
[dev-dependencies]
18+
tempfile = "3"

crates/doctor/src/lib.rs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
//! Spin doctor: check and automatically fix problems with Spin apps.
2+
#![deny(missing_docs)]
3+
4+
use std::{fmt::Debug, fs, future::Future, path::PathBuf, pin::Pin, sync::Arc};
5+
6+
use anyhow::{ensure, Context, Result};
7+
use async_trait::async_trait;
8+
use tokio::sync::Mutex;
9+
use toml_edit::Document;
10+
11+
/// Diagnoses for app manifest format problems.
12+
pub mod manifest;
13+
/// Test helpers.
14+
pub mod test;
15+
/// Diagnoses for Wasm source problems.
16+
pub mod wasm;
17+
18+
/// Configuration for an app to be checked for problems.
19+
pub struct Checkup {
20+
manifest_path: PathBuf,
21+
diagnostics: Vec<Box<dyn BoxingDiagnostic>>,
22+
}
23+
24+
impl Checkup {
25+
/// Return a new checkup for the app manifest at the given path.
26+
pub fn new(manifest_path: impl Into<PathBuf>) -> Self {
27+
let mut checkup = Self {
28+
manifest_path: manifest_path.into(),
29+
diagnostics: vec![],
30+
};
31+
checkup.add_diagnostic::<manifest::version::VersionDiagnostic>();
32+
checkup.add_diagnostic::<manifest::trigger::TriggerDiagnostic>();
33+
checkup.add_diagnostic::<wasm::missing::WasmMissingDiagnostic>();
34+
checkup
35+
}
36+
37+
/// Add a detectable problem to this checkup.
38+
pub fn add_diagnostic<D: Diagnostic + Default + 'static>(&mut self) -> &mut Self {
39+
self.diagnostics.push(Box::<D>::default());
40+
self
41+
}
42+
43+
fn patient(&self) -> Result<PatientApp> {
44+
let path = &self.manifest_path;
45+
ensure!(
46+
path.is_file(),
47+
"No Spin app manifest file found at {path:?}"
48+
);
49+
50+
let contents = fs::read_to_string(path)
51+
.with_context(|| format!("Couldn't read Spin app manifest file at {path:?}"))?;
52+
53+
let manifest_doc: Document = contents
54+
.parse()
55+
.with_context(|| format!("Couldn't parse manifest file at {path:?} as valid TOML"))?;
56+
57+
Ok(PatientApp {
58+
manifest_path: path.into(),
59+
manifest_doc,
60+
})
61+
}
62+
63+
/// Find problems with the configured app, calling the given closure with
64+
/// each problem found.
65+
pub async fn for_each_diagnosis<F>(&self, mut f: F) -> Result<usize>
66+
where
67+
F: for<'a> FnMut(
68+
Box<dyn Diagnosis + 'static>,
69+
&'a mut PatientApp,
70+
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>,
71+
{
72+
let patient = Arc::new(Mutex::new(self.patient()?));
73+
let mut count = 0;
74+
for diagnostic in &self.diagnostics {
75+
let patient = patient.clone();
76+
let diags = diagnostic
77+
.diagnose_boxed(&*patient.lock().await)
78+
.await
79+
.unwrap_or_else(|err| {
80+
tracing::debug!("Diagnose failed: {err:?}");
81+
vec![]
82+
});
83+
count += diags.len();
84+
for diag in diags {
85+
let mut patient = patient.lock().await;
86+
f(diag, &mut patient).await?;
87+
}
88+
}
89+
Ok(count)
90+
}
91+
}
92+
93+
/// An app "patient" to be checked for problems.
94+
#[derive(Clone)]
95+
pub struct PatientApp {
96+
/// Path to an app manifest file.
97+
pub manifest_path: PathBuf,
98+
/// Parsed app manifest TOML document.
99+
pub manifest_doc: Document,
100+
}
101+
102+
/// The Diagnose trait implements the detection of a particular Spin app problem.
103+
#[async_trait]
104+
pub trait Diagnostic: Send + Sync {
105+
/// A [`Diagnosis`] representing the problem(s) this can detect.
106+
type Diagnosis: Diagnosis;
107+
108+
/// Check the given [`Patient`], returning any problem(s) found.
109+
///
110+
/// If multiple _independently addressable_ problems are found, this may
111+
/// return multiple instances. If two "logically separate" problems would
112+
/// have the same fix, they should be represented with the same instance.
113+
async fn diagnose(&self, patient: &PatientApp) -> Result<Vec<Self::Diagnosis>>;
114+
}
115+
116+
/// The Diagnosis trait represents a detected problem with a Spin app.
117+
pub trait Diagnosis: Debug + Send + Sync + 'static {
118+
/// Return a human-friendly description of this problem.
119+
fn description(&self) -> String;
120+
121+
/// Return true if this problem is "critical", i.e. if the app's
122+
/// configuration or environment is invalid. Return false for
123+
/// "non-critical" problems like deprecations.
124+
fn is_critical(&self) -> bool {
125+
true
126+
}
127+
128+
/// Return a [`Treatment`] that can (potentially) fix this problem, or
129+
/// None if there is no automatic fix.
130+
fn treatment(&self) -> Option<&dyn Treatment> {
131+
None
132+
}
133+
}
134+
135+
/// The Treatment trait represents a (potential) fix for a detected problem.
136+
#[async_trait]
137+
pub trait Treatment: Sync {
138+
/// Return a short (single line) description of what this fix will do, as
139+
/// an imperative, e.g. "Upgrade the library".
140+
fn summary(&self) -> String;
141+
142+
/// Return a detailed description of what this fix will do, such as a file
143+
/// diff or list of commands to be executed.
144+
///
145+
/// May return `Err(DryRunNotSupported.into())` if no such description is
146+
/// available, which is the default implementation.
147+
async fn dry_run(&self, patient: &PatientApp) -> Result<String> {
148+
let _ = patient;
149+
Err(DryRunNotSupported.into())
150+
}
151+
152+
/// Attempt to fix this problem. Return Ok only if the problem is
153+
/// successfully fixed.
154+
async fn treat(&self, patient: &mut PatientApp) -> Result<()>;
155+
}
156+
157+
/// Error returned by [`Treatment::dry_run`] if dry run isn't supported.
158+
#[derive(Debug)]
159+
pub struct DryRunNotSupported;
160+
161+
impl std::fmt::Display for DryRunNotSupported {
162+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163+
write!(f, "dry run not implemented for this treatment")
164+
}
165+
}
166+
167+
impl std::error::Error for DryRunNotSupported {}
168+
169+
#[async_trait]
170+
trait BoxingDiagnostic {
171+
async fn diagnose_boxed(&self, patient: &PatientApp) -> Result<Vec<Box<dyn Diagnosis>>>;
172+
}
173+
174+
#[async_trait]
175+
impl<Factory: Diagnostic> BoxingDiagnostic for Factory {
176+
async fn diagnose_boxed(&self, patient: &PatientApp) -> Result<Vec<Box<dyn Diagnosis>>> {
177+
Ok(self
178+
.diagnose(patient)
179+
.await?
180+
.into_iter()
181+
.map(|diag| Box::new(diag) as Box<dyn Diagnosis>)
182+
.collect())
183+
}
184+
}

0 commit comments

Comments
 (0)