use std::path::PathBuf;
use std::time::Duration;
use anyhow::Context;
use bv_core::manifest::Manifest;
use bv_runtime::{ContainerRuntime, GpuProfile, ImageDigest, Mount, OciRef, RunSpec};
pub struct ConformanceResult {
pub tool_id: String,
pub passed: bool,
pub messages: Vec<String>,
pub duration: Duration,
}
impl ConformanceResult {
fn pass(tool_id: impl Into<String>, messages: Vec<String>, duration: Duration) -> Self {
Self {
tool_id: tool_id.into(),
passed: true,
messages,
duration,
}
}
fn fail(tool_id: impl Into<String>, messages: Vec<String>, duration: Duration) -> Self {
Self {
tool_id: tool_id.into(),
passed: false,
messages,
duration,
}
}
}
const DEFAULT_PROBES: &[&str] = &["--version", "-version", "--help", "-h", "-v", "version"];
pub fn run(
manifest: &Manifest,
image_digest: &str,
runtime: &dyn ContainerRuntime,
) -> anyhow::Result<ConformanceResult> {
let tool = &manifest.tool;
let start = std::time::Instant::now();
let failures = check_binaries(manifest, image_digest, runtime);
let duration = start.elapsed();
if failures.is_empty() {
Ok(ConformanceResult::pass(
&tool.id,
vec!["all binaries responded to smoke probes".into()],
duration,
))
} else {
Ok(ConformanceResult::fail(&tool.id, failures, duration))
}
}
fn check_binaries(
manifest: &Manifest,
image_digest: &str,
runtime: &dyn ContainerRuntime,
) -> Vec<String> {
let tool = &manifest.tool;
let binaries = tool.effective_binaries();
let mut image: OciRef = match tool.image.reference.parse() {
Ok(r) => r,
Err(_) => return vec!["invalid image reference; cannot run smoke check".into()],
};
image.tag = None;
image.digest = Some(image_digest.to_string());
let tmp = match tempfile::TempDir::new() {
Ok(t) => t,
Err(e) => return vec![format!("failed to create temp workspace: {e}")],
};
let smoke = tool.smoke.clone().unwrap_or_default();
let mut failures = Vec::new();
for binary in binaries {
if smoke.skip.iter().any(|s| s == binary) {
continue;
}
let probes: Vec<String> = match smoke.probes.get(binary) {
Some(probe) => vec![probe.clone()],
None => DEFAULT_PROBES.iter().map(|s| s.to_string()).collect(),
};
let mut passed = false;
for probe in &probes {
let mut command = vec![binary.to_string()];
if !probe.is_empty() {
command.push(probe.clone());
}
let spec = RunSpec {
image: image.clone(),
command,
env: Default::default(),
mounts: vec![Mount {
host_path: tmp.path().to_path_buf(),
container_path: PathBuf::from("/workspace"),
read_only: false,
}],
gpu: GpuProfile { spec: None },
working_dir: Some(PathBuf::from("/workspace")),
};
if let Ok(outcome) = runtime.run(&spec)
&& outcome.exit_code == 0
{
passed = true;
break;
}
}
if !passed {
let probe_list = probes
.iter()
.map(|p| {
if p.is_empty() {
"(no args)".into()
} else {
format!("'{p}'")
}
})
.collect::<Vec<_>>()
.join(" / ");
failures.push(format!(
"binary '{binary}' did not respond to {probe_list}.\n \
If this is expected (no version/help arg), add it to [tool.smoke].skip\n \
in the manifest. Or pin the right probe via [tool.smoke].probes."
));
}
}
failures
}
pub fn verify_image_reachable(
manifest: &Manifest,
runtime: &dyn ContainerRuntime,
) -> anyhow::Result<ImageDigest> {
let oci_ref: OciRef = manifest
.tool
.image
.reference
.parse()
.map_err(|e| anyhow::anyhow!("invalid image ref: {e}"))?;
let reporter = bv_runtime::NoopProgress;
runtime
.pull(&oci_ref, &reporter)
.with_context(|| format!("failed to pull image for '{}'", manifest.tool.id))
}