Skip to content

Commit

Permalink
Add 'spin doctor' subcommand
Browse files Browse the repository at this point in the history
Signed-off-by: Lann Martin <lann.martin@fermyon.com>
  • Loading branch information
lann committed May 1, 2023
1 parent 2669251 commit 2e838aa
Show file tree
Hide file tree
Showing 22 changed files with 1,078 additions and 25 deletions.
72 changes: 69 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 2 additions & 21 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ spin-app = { path = "crates/app" }
spin-bindle = { path = "crates/bindle" }
spin-build = { path = "crates/build" }
spin-config = { path = "crates/config" }
spin-doctor = { path = "crates/doctor" }
spin-trigger-http = { path = "crates/trigger-http" }
spin-loader = { path = "crates/loader" }
spin-manifest = { path = "crates/manifest" }
Expand Down Expand Up @@ -98,29 +99,9 @@ fermyon-platform = []

[workspace]
members = [
"crates/app",
"crates/bindle",
"crates/build",
"crates/config",
"crates/core",
"crates/http",
"crates/loader",
"crates/manifest",
"crates/oci",
"crates/outbound-http",
"crates/outbound-redis",
"crates/key-value",
"crates/key-value-sqlite",
"crates/key-value-redis",
"crates/plugins",
"crates/redis",
"crates/templates",
"crates/testing",
"crates/trigger",
"crates/trigger-http",
"crates/*",
"sdk/rust",
"sdk/rust/macro",
"crates/e2e-testing"
]

[workspace.dependencies]
Expand Down
18 changes: 18 additions & 0 deletions crates/doctor/Cargo.toml
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"
163 changes: 163 additions & 0 deletions crates/doctor/src/lib.rs
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)
}
42 changes: 42 additions & 0 deletions crates/doctor/src/manifest.rs
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:?}"))
}
}
Loading

0 comments on commit 2e838aa

Please sign in to comment.