-
Notifications
You must be signed in to change notification settings - Fork 260
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Lann Martin <lann.martin@fermyon.com>
- Loading branch information
Showing
22 changed files
with
1,078 additions
and
25 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[package] | ||
name = "spin-doctor" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[dependencies] | ||
anyhow = "1" | ||
async-trait = "0.1" | ||
serde = { version = "1", features = ["derive"] } | ||
similar = "2" | ||
spin-loader = { path = "../loader" } | ||
tokio = "1" | ||
toml = "0.7" | ||
toml_edit = "0.19" | ||
tracing = { workspace = true } | ||
|
||
[dev-dependencies] | ||
tempfile = "3" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
//! Spin doctor: check and automatically fix problems with Spin apps. | ||
#![deny(missing_docs)] | ||
|
||
use std::{fmt::Debug, fs, future::Future, path::PathBuf, pin::Pin, process::Command, sync::Arc}; | ||
|
||
use anyhow::{ensure, Context, Result}; | ||
use async_trait::async_trait; | ||
use tokio::sync::Mutex; | ||
use toml_edit::Document; | ||
|
||
/// Diagnoses for app manifest format problems. | ||
pub mod manifest; | ||
/// Test helpers. | ||
pub mod test; | ||
/// Diagnoses for Wasm source problems. | ||
pub mod wasm; | ||
|
||
/// Configuration for an app to be checked for problems. | ||
pub struct Checkup { | ||
manifest_path: PathBuf, | ||
diagnose_fns: Vec<DiagnoseFn>, | ||
} | ||
|
||
type DiagnoseFut<'a> = | ||
Pin<Box<dyn Future<Output = Result<Vec<Box<dyn Diagnosis + 'static>>>> + 'a>>; | ||
type DiagnoseFn = for<'a> fn(&'a PatientApp) -> DiagnoseFut<'a>; | ||
|
||
impl Checkup { | ||
/// Return a new checkup for the app manifest at the given path. | ||
pub fn new(manifest_path: impl Into<PathBuf>) -> Self { | ||
let mut config = Self { | ||
manifest_path: manifest_path.into(), | ||
diagnose_fns: vec![], | ||
}; | ||
config.add_diagnose::<manifest::version::VersionDiagnosis>(); | ||
config.add_diagnose::<manifest::trigger::TriggerDiagnosis>(); | ||
config.add_diagnose::<wasm::missing::WasmMissing>(); | ||
config | ||
} | ||
|
||
/// Add a detectable problem to this checkup. | ||
pub fn add_diagnose<D: Diagnose + 'static>(&mut self) -> &mut Self { | ||
self.diagnose_fns.push(diagnose_boxed::<D>); | ||
self | ||
} | ||
|
||
fn patient(&self) -> Result<PatientApp> { | ||
let path = &self.manifest_path; | ||
ensure!( | ||
path.is_file(), | ||
"No Spin app manifest file found at {path:?}" | ||
); | ||
|
||
let contents = fs::read_to_string(path) | ||
.with_context(|| format!("Couldn't read Spin app manifest file at {path:?}"))?; | ||
|
||
let manifest_doc: Document = contents | ||
.parse() | ||
.with_context(|| format!("Couldn't parse manifest file at {path:?} as valid TOML"))?; | ||
|
||
Ok(PatientApp { | ||
manifest_path: path.into(), | ||
manifest_doc, | ||
}) | ||
} | ||
|
||
/// Find problems with the configured app, calling the given closure with | ||
/// each problem found. | ||
pub async fn for_each_diagnosis<F>(&self, mut f: F) -> Result<usize> | ||
where | ||
F: for<'a> FnMut( | ||
Box<dyn Diagnosis + 'static>, | ||
&'a mut PatientApp, | ||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>, | ||
{ | ||
let patient = Arc::new(Mutex::new(self.patient()?)); | ||
let mut count = 0; | ||
for diagnose in &self.diagnose_fns { | ||
let patient = patient.clone(); | ||
let diags = diagnose(&*patient.lock().await) | ||
.await | ||
.unwrap_or_else(|err| { | ||
tracing::debug!("Diagnose failed: {err:?}"); | ||
vec![] | ||
}); | ||
count += diags.len(); | ||
for diag in diags { | ||
let mut patient = patient.lock().await; | ||
f(diag, &mut patient).await?; | ||
} | ||
} | ||
Ok(count) | ||
} | ||
} | ||
|
||
/// An app "patient" to be checked for problems. | ||
#[derive(Clone)] | ||
pub struct PatientApp { | ||
/// Path to an app manifest file. | ||
pub manifest_path: PathBuf, | ||
/// Parsed app manifest TOML document. | ||
pub manifest_doc: Document, | ||
} | ||
|
||
/// The Diagnose trait implements the detection of a particular Spin app problem. | ||
#[async_trait] | ||
pub trait Diagnose: Diagnosis + Send + Sized + 'static { | ||
/// Check the given [`Patient`], returning any problem(s) found. | ||
async fn diagnose(patient: &PatientApp) -> Result<Vec<Self>>; | ||
} | ||
|
||
fn diagnose_boxed<D: Diagnose>(patient: &PatientApp) -> DiagnoseFut { | ||
Box::pin(async { | ||
let diags = D::diagnose(patient).await?; | ||
Ok(diags.into_iter().map(|diag| Box::new(diag) as _).collect()) | ||
}) | ||
} | ||
|
||
/// The Diagnosis trait represents a detected problem with a Spin app. | ||
pub trait Diagnosis: Debug + Send + Sync { | ||
/// Return a human-friendly description of this problem. | ||
fn description(&self) -> String; | ||
|
||
/// Return true if this problem is "critical", i.e. if the app's | ||
/// configuration or environment is invalid. Return false for | ||
/// "non-critical" problems like deprecations. | ||
fn is_critical(&self) -> bool { | ||
true | ||
} | ||
|
||
/// Return a [`Treatment`] that can (potentially) fix this problem, or | ||
/// None if there is no automatic fix. | ||
fn treatment(&self) -> Option<&dyn Treatment> { | ||
None | ||
} | ||
} | ||
|
||
/// The Treatment trait represents a (potential) fix for a detected problem. | ||
#[async_trait] | ||
pub trait Treatment: Sync { | ||
/// Return a human-readable description of what this treatment will do to | ||
/// fix the problem, such as a file diff. | ||
async fn description(&self, patient: &PatientApp) -> Result<String>; | ||
|
||
/// Attempt to fix this problem. Return Ok only if the problem is | ||
/// successfully fixed. | ||
async fn treat(&self, patient: &mut PatientApp) -> Result<()>; | ||
} | ||
|
||
const SPIN_BIN_PATH: &str = "SPIN_BIN_PATH"; | ||
|
||
/// Return a [`Command`] targeting the `spin` binary. The `spin` path is | ||
/// resolved to the first of these that is available: | ||
/// - the `SPIN_BIN_PATH` environment variable | ||
/// - the current executable ([`std::env::current_exe`]) | ||
/// - the constant `"spin"` (resolved by e.g. `$PATH`) | ||
pub fn spin_command() -> Command { | ||
let spin_path = std::env::var_os(SPIN_BIN_PATH) | ||
.map(PathBuf::from) | ||
.or_else(|| std::env::current_exe().ok()) | ||
.unwrap_or("spin".into()); | ||
Command::new(spin_path) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
use std::fs; | ||
|
||
use anyhow::{Context, Result}; | ||
use async_trait::async_trait; | ||
use toml_edit::Document; | ||
|
||
use crate::Treatment; | ||
|
||
/// Diagnose app manifest trigger config problems. | ||
pub mod trigger; | ||
/// Diagnose app manifest version problems. | ||
pub mod version; | ||
|
||
/// ManifestTreatment helps implement [`Treatment`]s for app manifest problems. | ||
#[async_trait] | ||
pub trait ManifestTreatment { | ||
/// Attempt to fix this problem. See [`Treatment::treat`]. | ||
async fn treat_manifest(&self, doc: &mut Document) -> Result<()>; | ||
} | ||
|
||
#[async_trait] | ||
impl<T: ManifestTreatment + Sync> Treatment for T { | ||
async fn description(&self, patient: &crate::PatientApp) -> Result<String> { | ||
let mut after_doc = patient.manifest_doc.clone(); | ||
self.treat_manifest(&mut after_doc).await?; | ||
let before = patient.manifest_doc.to_string(); | ||
let after = after_doc.to_string(); | ||
let diff = similar::udiff::unified_diff(Default::default(), &before, &after, 1, None); | ||
Ok(format!( | ||
"Apply the following diff to {:?}:\n{}", | ||
patient.manifest_path, diff | ||
)) | ||
} | ||
|
||
async fn treat(&self, patient: &mut crate::PatientApp) -> Result<()> { | ||
let doc = &mut patient.manifest_doc; | ||
self.treat_manifest(doc).await?; | ||
let path = &patient.manifest_path; | ||
fs::write(path, doc.to_string()) | ||
.with_context(|| format!("failed to write fixed manifest to {path:?}")) | ||
} | ||
} |
Oops, something went wrong.