diff --git a/crates/ecrou/src/download.rs b/crates/ecrou/src/download.rs index 482250a..8f68f17 100644 --- a/crates/ecrou/src/download.rs +++ b/crates/ecrou/src/download.rs @@ -8,7 +8,7 @@ use std::path::{Path, PathBuf}; use std::time::Duration; use crate::distro::{Distro, ImageSource}; -use crate::Result; +use crate::{Error, Result}; /// Path of the digest sidecar file for a cached OCI image. /// e.g. `foo.tar.gz` → `foo.tar.gz.digest` @@ -445,16 +445,16 @@ async fn try_download_oci_image( m_arch == arch && m_os == "linux" }) .ok_or_else(|| { - // List available architectures in the error message + // The typed variant rides at the top of the chain, so it + // survives into `crate::Error` for consumers to match on. let available: Vec<&str> = manifests .iter() .filter_map(|m| m["platform"]["architecture"].as_str()) .collect(); - anyhow!( - "No manifest found for architecture '{}'. Available: {}", - arch, - available.join(", ") - ) + anyhow::Error::new(Error::ArchNotAvailable { + arch: arch.to_string(), + available: available.join(", "), + }) })?; let manifest_digest = manifest_entry["digest"] diff --git a/crates/ecrou/src/error.rs b/crates/ecrou/src/error.rs index 30daf76..a640cb9 100644 --- a/crates/ecrou/src/error.rs +++ b/crates/ecrou/src/error.rs @@ -6,12 +6,20 @@ //! versus ordinary failures. Everything without a semantic variant //! surfaces as [`Error::Other`], which preserves the full anyhow context //! chain. +//! +//! Internal plumbing is anyhow all the way down; a semantic variant is +//! raised by constructing it as the top of an anyhow chain +//! (`anyhow::Error::new(Error::...)`). The [`From`] impl below recovers +//! exactly that, so the type a consumer matches on survives every `?` +//! between the producer and the public API. /// Errors returned by the ecrou library API. #[derive(Debug, thiserror::Error)] pub enum Error { /// No working KVM on this host, and ecrou has no software-emulation - /// fallback. The remediation names the usual causes in order. + /// fallback. The remediation names the usual causes in order; the + /// source carries the open(2) or ioctl error behind the probe + /// failure. #[error( "KVM is not available\n\n\ ecrou boots microVMs and needs /dev/kvm:\n\ @@ -21,7 +29,7 @@ pub enum Error { \x20 cat /sys/module/kvm_intel/parameters/nested\n\ \x20 access: sudo usermod -aG kvm $USER (re-login)" )] - KvmUnavailable, + KvmUnavailable(#[source] std::io::Error), /// The registry publishes no linux manifest for the requested /// architecture; `available` lists the architectures it does serve. @@ -30,7 +38,19 @@ pub enum Error { /// Any other failure, preserving the full context chain. #[error(transparent)] - Other(#[from] anyhow::Error), + Other(anyhow::Error), +} + +impl From for Error { + fn from(err: anyhow::Error) -> Self { + // A semantic variant riding at the top of an anyhow chain + // travels intact; anything else wraps as Other and keeps the + // whole context chain for Display. + match err.downcast::() { + Ok(typed) => typed, + Err(rest) => Error::Other(rest), + } + } } /// Result alias for the public library API: `Result` carries [`Error`]. @@ -42,10 +62,29 @@ mod tests { #[test] fn kvm_unavailable_renders_remediation() { - let msg = Error::KvmUnavailable.to_string(); + let msg = + Error::KvmUnavailable(std::io::Error::from_raw_os_error(libc::EACCES)).to_string(); assert!(msg.contains("KVM is not available")); assert!(msg.contains("modprobe kvm_intel")); assert!(msg.contains("usermod -aG kvm")); + // The open(2) detail stays reachable for {err:#} printing, + // which the CLI does over an anyhow wrapping of this error. + let err = Error::KvmUnavailable(std::io::Error::from_raw_os_error(libc::EACCES)); + let printed = format!("{:#}", anyhow::Error::new(err)); + assert!(printed.contains("Permission denied"), "{printed}"); + } + + #[test] + fn a_typed_error_survives_the_anyhow_conversion() { + let raised = + anyhow::Error::new(Error::KvmUnavailable(std::io::Error::other("no /dev/kvm"))); + match Error::from(raised) { + Error::KvmUnavailable(_) => {} + other => panic!("typed variant lost through From: {other}"), + } + // A plain anyhow error still wraps as Other with its chain kept. + let plain = anyhow::anyhow!("download failed").context("fetching the image"); + assert!(matches!(Error::from(plain), Error::Other(_))); } #[test] diff --git a/crates/ecrou/src/krun.rs b/crates/ecrou/src/krun.rs index db08ff9..5a7f21f 100644 --- a/crates/ecrou/src/krun.rs +++ b/crates/ecrou/src/krun.rs @@ -13,7 +13,7 @@ use std::ffi::{c_char, CString}; use std::os::unix::ffi::OsStrExt; use std::path::Path; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use crate::Error; @@ -304,18 +304,22 @@ pub fn kvm_probe() -> Result<()> { .read(true) .write(true) .open("/dev/kvm") - .map_err(|err| anyhow::Error::from(err).context(Error::KvmUnavailable.to_string()))?; + // The variant rides at the top of the chain so it survives the + // conversion into `crate::Error`; the open(2) error stays + // reachable as its source. + .map_err(|err| anyhow::Error::new(Error::KvmUnavailable(err)))?; // Safety: the fd is open and an ioctl on /dev/kvm with // KVM_GET_API_VERSION reads nothing; only the kernel writes the int // return. let ret = unsafe { libc::ioctl(file.as_raw_fd(), KVM_GET_API_VERSION) }; if ret != KVM_API_VERSION_EXPECTED { - return Err(anyhow!( - "/dev/kvm reports API version {}, expected {}; the kernel module \ - and the userspace disagree", - ret, - KVM_API_VERSION_EXPECTED - )); + return Err(anyhow::Error::new(Error::KvmUnavailable( + std::io::Error::other(format!( + "/dev/kvm reports API version {}, expected {}; the kernel module \ + and the userspace disagree", + ret, KVM_API_VERSION_EXPECTED + )), + ))); } Ok(()) }