diff --git a/crates/spfs-cli/cmd-clean/src/cmd_clean.rs b/crates/spfs-cli/cmd-clean/src/cmd_clean.rs index 9e78ca5f10..a58b2866e6 100644 --- a/crates/spfs-cli/cmd-clean/src/cmd_clean.rs +++ b/crates/spfs-cli/cmd-clean/src/cmd_clean.rs @@ -167,7 +167,7 @@ impl CmdClean { // durable upper path's workdir that have 'd---------' permissions // from overlayfs. std::fs::remove_dir_all(durable_path.clone()) - .map_err(|err| spfs::Error::RuntimeWriteError(durable_path, err))?; + .map_err(|err| spfs::runtime::Error::RuntimeWriteError(durable_path, err))?; } return Ok(0); } diff --git a/crates/spfs-cli/cmd-render/src/cmd_render.rs b/crates/spfs-cli/cmd-render/src/cmd_render.rs index c7508623d3..b9c709b0b9 100644 --- a/crates/spfs-cli/cmd-render/src/cmd_render.rs +++ b/crates/spfs-cli/cmd-render/src/cmd_render.rs @@ -6,6 +6,7 @@ use clap::Parser; use clap::builder::TypedValueParser; use miette::{Context, Result}; use spfs::prelude::*; +use spfs::runtime::Error as RuntimeError; use spfs::storage::fallback::FallbackProxy; use spfs::{Error, RenderResult, graph}; use spfs_cli_common::{self as cli, CommandName, HasRepositoryArgs}; @@ -97,15 +98,15 @@ impl CmdRender { ) -> Result { tokio::fs::create_dir_all(&target) .await - .map_err(|err| Error::RuntimeWriteError(target.to_owned(), err))?; + .map_err(|err| RuntimeError::RuntimeWriteError(target.to_owned(), err))?; let target_dir = tokio::task::block_in_place(|| dunce::canonicalize(target)) .map_err(|err| Error::InvalidPath(target.to_owned(), err))?; if tokio::fs::read_dir(&target_dir) .await - .map_err(|err| Error::RuntimeReadError(target_dir.clone(), err))? + .map_err(|err| RuntimeError::RuntimeReadError(target_dir.clone(), err))? .next_entry() .await - .map_err(|err| Error::RuntimeReadError(target_dir.clone(), err))? + .map_err(|err| RuntimeError::RuntimeReadError(target_dir.clone(), err))? .is_some() && !self.allow_existing { diff --git a/crates/spfs-cli/common/src/args.rs b/crates/spfs-cli/common/src/args.rs index 382156a825..2b002ac2f0 100644 --- a/crates/spfs-cli/common/src/args.rs +++ b/crates/spfs-cli/common/src/args.rs @@ -679,7 +679,10 @@ macro_rules! handle_result { tracing::error!("Out of disk space: {msg}"); Ok(1) } - Some(spfs::Error::RuntimeWriteError(path, io_err)) + Some(spfs::Error::Runtime(spfs::runtime::Error::RuntimeWriteError( + path, + io_err, + ))) | Some(spfs::Error::StorageWriteError(_, path, io_err)) if std::matches!(io_err.os_error(), Some($crate::__private::libc::ENOSPC)) => { @@ -706,11 +709,11 @@ macro_rules! handle_result { pub fn capture_if_relevant(err: &Error) { match err.root_cause().downcast_ref::() { - Some(spfs::Error::NoActiveRuntime) => (), + Some(spfs::Error::Runtime(spfs::runtime::Error::NoActiveRuntime)) => (), Some(spfs::Error::UnknownObject(_)) => (), Some(spfs::Error::UnknownReference(_)) => (), Some(spfs::Error::AmbiguousReference(_)) => (), - Some(spfs::Error::NothingToCommit) => (), + Some(spfs::Error::Runtime(spfs::runtime::Error::NothingToCommit)) => (), _ => { #[cfg(feature = "sentry")] sentry_miette::capture_miette(err); diff --git a/crates/spfs-cli/main/src/cmd_commit.rs b/crates/spfs-cli/main/src/cmd_commit.rs index f8e75b2d59..13dc233994 100644 --- a/crates/spfs-cli/main/src/cmd_commit.rs +++ b/crates/spfs-cli/main/src/cmd_commit.rs @@ -134,7 +134,7 @@ impl CmdCommit { if let Some(path) = &self.path { let manifest = committer.commit_dir(path).await?; if manifest.is_empty() && !self.allow_empty { - return Err(spfs::Error::NothingToCommit); + return Err(spfs::runtime::Error::NothingToCommit.into()); } return Ok(repo .create_layer(&manifest.to_graph_manifest()) diff --git a/crates/spfs-cli/main/src/cmd_edit.rs b/crates/spfs-cli/main/src/cmd_edit.rs index 641623860a..f0375dfba0 100644 --- a/crates/spfs-cli/main/src/cmd_edit.rs +++ b/crates/spfs-cli/main/src/cmd_edit.rs @@ -4,7 +4,7 @@ use clap::Args; use miette::Result; -use spfs::Error; +use spfs::runtime::Error as RuntimeError; /// Make the current runtime editable #[derive(Debug, Args)] @@ -38,7 +38,7 @@ impl CmdEdit { if !self.off { match spfs::make_active_runtime_editable().await { Ok(_) => tracing::info!("edit mode enabled"), - Err(Error::RuntimeAlreadyEditable) => {} + Err(spfs::Error::Runtime(RuntimeError::RuntimeAlreadyEditable)) => {} Err(err) => { return Err(err.into()); } diff --git a/crates/spfs-cli/main/src/cmd_info.rs b/crates/spfs-cli/main/src/cmd_info.rs index 8e463f90e3..c820e2fc2b 100644 --- a/crates/spfs-cli/main/src/cmd_info.rs +++ b/crates/spfs-cli/main/src/cmd_info.rs @@ -272,7 +272,7 @@ impl CmdInfo { let found = match spfs::find_path::find_path_providers_in_spfs_runtime(filepath, repo).await { Ok(f) => f, - Err(spfs::Error::NoActiveRuntime) => { + Err(spfs::Error::Runtime(spfs::runtime::Error::NoActiveRuntime)) => { in_a_runtime = false; Vec::new() } diff --git a/crates/spfs-cli/main/src/cmd_write.rs b/crates/spfs-cli/main/src/cmd_write.rs index e45ba437df..3fe5dafbc4 100644 --- a/crates/spfs-cli/main/src/cmd_write.rs +++ b/crates/spfs-cli/main/src/cmd_write.rs @@ -8,8 +8,8 @@ use std::path::PathBuf; use clap::Args; use miette::Result; -use spfs::Error; use spfs::prelude::*; +use spfs::runtime::Error as RuntimeError; use spfs::tracking::BlobReadExt; use spfs_cli_common as cli; @@ -40,12 +40,12 @@ impl CmdWrite { Some(file) => { let handle = tokio::fs::File::open(&file) .await - .map_err(|err| Error::RuntimeWriteError(file.clone(), err))?; + .map_err(|err| RuntimeError::RuntimeWriteError(file.clone(), err))?; #[cfg(unix)] let mode = handle .metadata() .await - .map_err(|err| Error::RuntimeWriteError(file.clone(), err))? + .map_err(|err| RuntimeError::RuntimeWriteError(file.clone(), err))? .permissions() .mode(); #[cfg(windows)] diff --git a/crates/spfs-vfs/src/winfsp/router.rs b/crates/spfs-vfs/src/winfsp/router.rs index 4bf14d8814..fb4ea86e11 100644 --- a/crates/spfs-vfs/src/winfsp/router.rs +++ b/crates/spfs-vfs/src/winfsp/router.rs @@ -69,7 +69,7 @@ impl Router { tracing::info!(%root_pid, env_spec=%env_spec.to_string(),"mounted"); let mut routes = self.routes.write().expect("lock is never poisoned"); if routes.contains_key(&root_pid) { - return Err(spfs::Error::RuntimeExists(root_pid.to_string())); + return Err(spfs::runtime::Error::RuntimeExists(root_pid.to_string()).into()); } routes.insert(root_pid, Arc::new(mount)); Ok(()) diff --git a/crates/spfs/src/commit.rs b/crates/spfs/src/commit.rs index 6cae9b9a7b..185da9ef65 100644 --- a/crates/spfs/src/commit.rs +++ b/crates/spfs/src/commit.rs @@ -14,6 +14,7 @@ use spfs_encoding::prelude::*; use super::status::remount_runtime; use crate::prelude::*; +use crate::runtime::Error as RuntimeError; use crate::tracking::{BlobHasher, BlobRead, ManifestBuilder, PathFilter}; use crate::{Error, Result, encoding, graph, runtime, storage, tracking}; @@ -198,7 +199,7 @@ where runtime: &mut runtime::Runtime, ) -> Result { if manifest.is_empty() && !self.allow_empty { - return Err(Error::NothingToCommit); + return Err(RuntimeError::NothingToCommit.into()); } let layer = self .repo @@ -208,7 +209,7 @@ where // Don't bother putting the empty layer on the stack, the goal // with allow_empty is to create an empty manifest. if !runtime.push_digest(layer.digest()?) { - return Err(Error::NothingToCommit); + return Err(RuntimeError::NothingToCommit.into()); } runtime.status.editable = false; runtime.save_state_to_storage().await?; @@ -220,13 +221,13 @@ where /// Commit the full layer stack and working files to a new platform. pub async fn commit_platform(&self, runtime: &mut runtime::Runtime) -> Result { match self.commit_layer(runtime).await { - Ok(_) | Err(Error::NothingToCommit) => (), + Ok(_) | Err(Error::Runtime(RuntimeError::NothingToCommit)) => (), Err(err) => return Err(err), } runtime.reload_state_from_storage().await?; if runtime.status.stack.is_empty() && !self.allow_empty { - Err(Error::NothingToCommit) + Err(RuntimeError::NothingToCommit.into()) } else { self.repo .create_platform(runtime.status.stack.clone()) diff --git a/crates/spfs/src/commit_test.rs b/crates/spfs/src/commit_test.rs index b6167246b6..300c260744 100644 --- a/crates/spfs/src/commit_test.rs +++ b/crates/spfs/src/commit_test.rs @@ -7,6 +7,7 @@ use rstest::rstest; use super::Committer; use crate::Error; use crate::fixtures::*; +use crate::runtime::Error as RuntimeError; #[rstest] #[tokio::test] @@ -27,12 +28,12 @@ async fn test_commit_empty(tmpdir: tempfile::TempDir) { rt.ensure_required_directories().await.unwrap(); let committer = Committer::new(&repo); match committer.commit_layer(&mut rt).await { - Err(Error::NothingToCommit) => {} + Err(Error::Runtime(RuntimeError::NothingToCommit)) => {} res => panic!("expected nothing to commit, got {res:?}"), } match committer.commit_platform(&mut rt).await { - Err(Error::NothingToCommit) => {} + Err(Error::Runtime(RuntimeError::NothingToCommit)) => {} res => panic!("expected nothing to commit, got {res:?}"), } } diff --git a/crates/spfs/src/env.rs b/crates/spfs/src/env.rs index 27598d8e72..4ff0bfaa58 100644 --- a/crates/spfs/src/env.rs +++ b/crates/spfs/src/env.rs @@ -21,6 +21,7 @@ use linux_syscall::{ use super::runtime; use crate::config::OverlayFsOptions; +use crate::runtime::Error as RuntimeError; use crate::{Error, Result, which}; pub const SPFS_DIR: &str = "/spfs"; @@ -248,7 +249,7 @@ impl RuntimeConfigurator { rt: &runtime::Runtime, ) -> Result> { let Some(runtime_ns) = &rt.config.mount_namespace else { - return Err(Error::NoActiveRuntime); + return Err(RuntimeError::NoActiveRuntime.into()); }; // Safety: we are going to validate that this is the // expected namespace for the provided runtime and so @@ -283,7 +284,7 @@ impl RuntimeConfigurator { check_can_join()?; let pid = match rt.status.owner { - None => return Err(Error::RuntimeNotInitialized(rt.name().into())), + None => return Err(RuntimeError::RuntimeNotInitialized(rt.name().into()).into()), Some(pid) => pid, }; @@ -296,11 +297,12 @@ impl RuntimeConfigurator { Ok(file) => file, Err(err) => { return match err.kind() { - std::io::ErrorKind::NotFound => Err(Error::UnknownRuntime { + std::io::ErrorKind::NotFound => Err(RuntimeError::UnknownRuntime { runtime: rt.name().into(), source: Box::new(err), - }), - _ => Err(Error::RuntimeReadError(ns_path, err)), + } + .into()), + _ => Err(RuntimeError::RuntimeReadError(ns_path, err).into()), }; } }; @@ -445,11 +447,11 @@ where pub fn ensure_mount_targets_exist(&self, config: &runtime::Config) -> Result<()> { tracing::debug!("ensuring mount targets exist..."); runtime::makedirs_with_perms(SPFS_DIR, 0o777) - .map_err(|source| Error::CouldNotCreateSpfsRoot { source })?; + .map_err(|source| RuntimeError::CouldNotCreateSpfsRoot { source })?; if let Some(dir) = &config.runtime_dir { runtime::makedirs_with_perms(dir, 0o777) - .map_err(|err| Error::RuntimeWriteError(dir.clone(), err))? + .map_err(|err| RuntimeError::RuntimeWriteError(dir.clone(), err))? } Ok(()) } @@ -639,7 +641,7 @@ where if let Some(parent) = fullpath.parent() { tracing::trace!(?parent, "build parent dir for mask"); runtime::makedirs_with_perms(parent, 0o777) - .map_err(|err| Error::RuntimeWriteError(parent.to_owned(), err))?; + .map_err(|err| RuntimeError::RuntimeWriteError(parent.to_owned(), err))?; } tracing::trace!(?node.path, "Creating file mask"); @@ -649,11 +651,13 @@ where continue; } if meta.is_file() { - std::fs::remove_file(&fullpath) - .map_err(|err| Error::RuntimeWriteError(fullpath.clone(), err))?; + std::fs::remove_file(&fullpath).map_err(|err| { + RuntimeError::RuntimeWriteError(fullpath.clone(), err) + })?; } else { - std::fs::remove_dir_all(&fullpath) - .map_err(|err| Error::RuntimeWriteError(fullpath.clone(), err))?; + std::fs::remove_dir_all(&fullpath).map_err(|err| { + RuntimeError::RuntimeWriteError(fullpath.clone(), err) + })?; } } @@ -677,7 +681,7 @@ where continue; } let existing = std::fs::symlink_metadata(&fullpath) - .map_err(|err| Error::RuntimeReadError(fullpath.clone(), err))?; + .map_err(|err| RuntimeError::RuntimeReadError(fullpath.clone(), err))?; if existing.permissions().mode() != node.entry.mode && let Err(err) = std::fs::set_permissions( &fullpath, @@ -687,7 +691,9 @@ where match err.kind() { std::io::ErrorKind::NotFound => continue, _ => { - return Err(Error::RuntimeSetPermissionsError(fullpath, err)); + return Err( + RuntimeError::RuntimeSetPermissionsError(fullpath, err).into() + ); } } } @@ -728,10 +734,11 @@ where match runtime.config.mount_backend { runtime::MountBackend::FuseOnly | runtime::MountBackend::WinFsp => { // a vfs-only runtime cannot be change to durable - return Err(Error::RuntimeChangeToDurableError(format!( + return Err(RuntimeError::RuntimeChangeToDurableError(format!( "{} backend does not support durable runtimes", runtime.config.mount_backend - ))); + )) + .into()); } runtime::MountBackend::OverlayFsWithFuse | runtime::MountBackend::OverlayFsWithRenders => {} @@ -755,19 +762,21 @@ where let src_dir = match old_upper_dir.to_str() { Some(path) => path, None => { - return Err(Error::RuntimeChangeToDurableError(format!( + return Err(RuntimeError::RuntimeChangeToDurableError(format!( "current upper_dir '{}' has invalid characters", old_upper_dir.display() - ))); + )) + .into()); } }; let dest_dir = match new_path.to_str() { Some(path) => path, None => { - return Err(Error::RuntimeChangeToDurableError(format!( + return Err(RuntimeError::RuntimeChangeToDurableError(format!( "new upper_dir '{}' has invalid characters", new_path.display() - ))); + )) + .into()); } }; @@ -775,9 +784,10 @@ where let cmd_path = match which("rsync") { Some(cmd) => cmd, None => { - return Err(Error::RuntimeChangeToDurableError( + return Err(RuntimeError::RuntimeChangeToDurableError( "rsync is not available on this host".to_string(), - )); + ) + .into()); } }; @@ -793,16 +803,19 @@ where tracing::info!("runtime saved as durable"); Ok(0) } - Some(code) => Err(Error::RuntimeChangeToDurableError(format!( + Some(code) => Err(RuntimeError::RuntimeChangeToDurableError(format!( "rsync failed with exit code: {code}" - ))), - None => Err(Error::RuntimeChangeToDurableError( + )) + .into()), + None => Err(RuntimeError::RuntimeChangeToDurableError( "rsync was terminated by an unexpected signal".to_string(), - )), + ) + .into()), }, - Err(err) => Err(Error::RuntimeChangeToDurableError(format!( + Err(err) => Err(RuntimeError::RuntimeChangeToDurableError(format!( "rsync failed to run: {err}" - ))), + )) + .into()), } } diff --git a/crates/spfs/src/env_win.rs b/crates/spfs/src/env_win.rs index 2cb885ca4a..17f90f5242 100644 --- a/crates/spfs/src/env_win.rs +++ b/crates/spfs/src/env_win.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // https://github.com/spkenv/spk +use crate::runtime::Error as RuntimeError; use crate::tracking::EnvSpec; use crate::{Error, Result, runtime}; @@ -29,9 +30,10 @@ impl RuntimeConfigurator { /// Mount the provided runtime via the winfsp backend pub async fn mount_env_winfsp(&self, rt: &runtime::Runtime) -> Result<()> { let Some(root_pid) = rt.status.owner else { - return Err(Error::RuntimeNotInitialized( + return Err(RuntimeError::RuntimeNotInitialized( "Missing owner in runtime, cannot initialize".to_string(), - )); + ) + .into()); }; let env_spec = rt diff --git a/crates/spfs/src/error.rs b/crates/spfs/src/error.rs index 8ac760bd44..e7cb391303 100644 --- a/crates/spfs/src/error.rs +++ b/crates/spfs/src/error.rs @@ -13,7 +13,7 @@ use std::str::Utf8Error; use miette::Diagnostic; use thiserror::Error; -use crate::{encoding, graph, storage}; +use crate::{encoding, graph, runtime, storage}; #[derive(Diagnostic, Debug, Error)] #[diagnostic( @@ -50,6 +50,9 @@ pub enum Error { #[error(transparent)] #[diagnostic(forward(0))] GraphObject(#[from] super::graph::error::ObjectError), + #[error(transparent)] + #[diagnostic(forward(0))] + Runtime(#[from] runtime::Error), #[error("Invalid repository url: {0:?}")] InvalidRemoteUrl(#[from] url::ParseError), @@ -112,46 +115,6 @@ pub enum Error { )] UnknownRemoteName(String), - #[error("Nothing to commit, resulting filesystem would be empty")] - NothingToCommit, - #[error("No active runtime")] - NoActiveRuntime, - #[error("Runtime has not been initialized: {0}")] - RuntimeNotInitialized(String), - #[error("Runtime does not exist: {runtime}")] - UnknownRuntime { - runtime: String, - source: Box, - }, - #[error("Runtime already exists: {0}")] - RuntimeExists(String), - #[error( - "An existing runtime is using the same upper name ({upper_name}).\nTry another name, or connect to the runtime by running:\n\n spfs join {runtime_name} " - )] - RuntimeUpperDirAlreadyInUse { - upper_name: String, - runtime_name: String, - }, - #[error( - "This kind of repository does not support durable runtime paths. A FSRepository is required for that." - )] - DoesNotSupportDurableRuntimePath, - #[error("Runtime is already editable")] - RuntimeAlreadyEditable, - #[error("Runtime read error: {0}")] - RuntimeReadError(std::path::PathBuf, #[source] io::Error), - #[error("Runtime write error: {0}")] - RuntimeWriteError(std::path::PathBuf, #[source] io::Error), - #[error("Runtime set permissions error: {0}")] - RuntimeSetPermissionsError(std::path::PathBuf, #[source] io::Error), - #[error("Failed to create {} directory", crate::env::SPFS_DIR)] - #[diagnostic( - code("spfs::could_not_create_spfs_dir"), - help("If you have sudo/admin privileges, you can try creating it yourself") - )] - CouldNotCreateSpfsRoot { source: std::io::Error }, - #[error("Unable to make the runtime durable: {0}")] - RuntimeChangeToDurableError(String), #[error("Storage read error from {0} at {1}: {2}")] StorageReadError(&'static str, std::path::PathBuf, #[source] io::Error), #[error("Storage write error from {0} at {1}: {2}")] @@ -358,8 +321,7 @@ impl OsError for Error { Error::Encoding(encoding::Error::FailedRead(err)) => err.os_error(), Error::Encoding(encoding::Error::FailedWrite(err)) => err.os_error(), Error::ProcessSpawnError(_, err) => err.os_error(), - Error::RuntimeReadError(_, err) => err.os_error(), - Error::RuntimeWriteError(_, err) => err.os_error(), + Error::Runtime(err) => err.os_error(), Error::StorageReadError(_, _, err) => err.os_error(), Error::StorageWriteError(_, _, err) => err.os_error(), Error::Errno(_, errno) => Some(*errno), diff --git a/crates/spfs/src/find_path.rs b/crates/spfs/src/find_path.rs index 144de00a2e..daa5dbaee2 100644 --- a/crates/spfs/src/find_path.rs +++ b/crates/spfs/src/find_path.rs @@ -8,7 +8,8 @@ use spfs_encoding::Digest; use spfs_encoding::prelude::*; use crate::graph::{self, DatabaseView, Object}; -use crate::{Error, Result, env, status, storage, tracking}; +use crate::runtime::Error as RuntimeError; +use crate::{Result, env, status, storage, tracking}; /// Used for items in a list of spfs objects that contain a filepath. /// The parent containers down to the filepath will be graph objects. @@ -56,7 +57,7 @@ pub async fn find_path_providers_in_spfs_runtime( } } } else { - return Err(Error::NoActiveRuntime); + return Err(RuntimeError::NoActiveRuntime.into()); } Ok(found) diff --git a/crates/spfs/src/monitor.rs b/crates/spfs/src/monitor.rs index 5359b9a953..b0931427cf 100644 --- a/crates/spfs/src/monitor.rs +++ b/crates/spfs/src/monitor.rs @@ -16,6 +16,7 @@ use tokio_stream::wrappers::{IntervalStream, UnboundedReceiverStream}; use super::runtime; use crate::repeating_timeout::RepeatingTimeout; +use crate::runtime::Error as RuntimeError; use crate::{Error, OsError, Result}; pub const PROC_DIR: &str = "/proc"; @@ -85,7 +86,7 @@ pub fn spawn_monitor_for_runtime(rt: &runtime::Runtime) -> Result Result<()> { let pid = match rt.status.owner { - None => return Err(Error::RuntimeNotInitialized(rt.name().into())), + None => return Err(RuntimeError::RuntimeNotInitialized(rt.name().into()).into()), Some(pid) => pid, }; @@ -107,7 +108,7 @@ pub async fn wait_for_empty_runtime(rt: &runtime::Runtime, config: &crate::Confi // Only retry if the namespace couldn't be read because of EACCES. match error { - Error::RuntimeReadError(_, err) + Error::Runtime(RuntimeError::RuntimeReadError(_, err)) if matches!(err.kind(), std::io::ErrorKind::PermissionDenied) => { self.had_to_retry.store(true, Ordering::Relaxed); @@ -384,7 +385,7 @@ pub async fn identify_mount_namespace_of_process(pid: u32) -> Result Err(Error::RuntimeReadError(ns_path, err)), + _ => Err(RuntimeError::RuntimeReadError(ns_path, err).into()), }, } } @@ -395,11 +396,11 @@ pub async fn find_processes_and_mount_namespaces() -> Result()) { Some(Ok(pid)) => pid, @@ -425,11 +426,11 @@ async fn find_other_processes_in_mount_namespace(ns: &std::path::Path) -> Result let mut read_dir = tokio::fs::read_dir(PROC_DIR) .await - .map_err(|err| Error::RuntimeReadError(PROC_DIR.into(), err))?; + .map_err(|err| RuntimeError::RuntimeReadError(PROC_DIR.into(), err))?; while let Some(entry) = read_dir .next_entry() .await - .map_err(|err| Error::RuntimeReadError(PROC_DIR.into(), err))? + .map_err(|err| RuntimeError::RuntimeReadError(PROC_DIR.into(), err))? { let pid = match entry.file_name().to_str().map(|s| s.parse::()) { Some(Ok(pid)) => pid, @@ -446,7 +447,7 @@ async fn find_other_processes_in_mount_namespace(ns: &std::path::Path) -> Result Some(libc::EACCES) => continue, Some(libc::EPERM) => continue, _ => { - return Err(Error::RuntimeReadError(link_path, err)); + return Err(RuntimeError::RuntimeReadError(link_path, err).into()); } }, }; diff --git a/crates/spfs/src/runtime/error.rs b/crates/spfs/src/runtime/error.rs new file mode 100644 index 0000000000..ff17b2610b --- /dev/null +++ b/crates/spfs/src/runtime/error.rs @@ -0,0 +1,78 @@ +// Copyright (c) Contributors to the SPK project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/spkenv/spk + +#![allow(unused_assignments)] + +use std::io; + +use miette::Diagnostic; +use thiserror::Error; + +use crate::error::OsError; + +pub type Result = std::result::Result; + +#[derive(Diagnostic, Debug, Error)] +#[diagnostic( + url( + "https://spkenv.dev/error_codes#{}", + self.code().unwrap_or_else(|| Box::new("spfs::runtime")) + ) +)] +pub enum Error { + #[error("Nothing to commit, resulting filesystem would be empty")] + NothingToCommit, + #[error("No active runtime")] + NoActiveRuntime, + #[error("Runtime has not been initialized: {0}")] + RuntimeNotInitialized(String), + #[error("Runtime does not exist: {runtime}")] + UnknownRuntime { + runtime: String, + #[source] + source: Box, + }, + #[error("Runtime already exists: {0}")] + RuntimeExists(String), + #[error( + "An existing runtime is using the same upper name ({upper_name}).\nTry another name, or connect to the runtime by running:\n\n spfs join {runtime_name} " + )] + RuntimeUpperDirAlreadyInUse { + upper_name: String, + runtime_name: String, + }, + #[error( + "This kind of repository does not support durable runtime paths. A FSRepository is required for that." + )] + DoesNotSupportDurableRuntimePath, + #[error("Runtime is already editable")] + RuntimeAlreadyEditable, + #[error("Runtime read error: {0}")] + RuntimeReadError(std::path::PathBuf, #[source] io::Error), + #[error("Runtime write error: {0}")] + RuntimeWriteError(std::path::PathBuf, #[source] io::Error), + #[error("Runtime set permissions error: {0}")] + RuntimeSetPermissionsError(std::path::PathBuf, #[source] io::Error), + #[error("Failed to create {} directory", crate::env::SPFS_DIR)] + #[diagnostic( + code("spfs::could_not_create_spfs_dir"), + help("If you have sudo/admin privileges, you can try creating it yourself") + )] + CouldNotCreateSpfsRoot { + #[source] + source: std::io::Error, + }, + #[error("Unable to make the runtime durable: {0}")] + RuntimeChangeToDurableError(String), +} + +impl OsError for Error { + fn os_error(&self) -> Option { + match self { + Error::RuntimeReadError(_, err) => err.os_error(), + Error::RuntimeWriteError(_, err) => err.os_error(), + _ => None, + } + } +} diff --git a/crates/spfs/src/runtime/mod.rs b/crates/spfs/src/runtime/mod.rs index 4aa4fd16e5..d4dc8231ae 100644 --- a/crates/spfs/src/runtime/mod.rs +++ b/crates/spfs/src/runtime/mod.rs @@ -4,6 +4,8 @@ //! Handles the setup and initialization of runtime environments +pub mod error; +pub use error::Error; pub mod live_layer; #[cfg(unix)] pub mod overlayfs; diff --git a/crates/spfs/src/runtime/storage.rs b/crates/spfs/src/runtime/storage.rs index d036ccee8a..a6f6da1d26 100644 --- a/crates/spfs/src/runtime/storage.rs +++ b/crates/spfs/src/runtime/storage.rs @@ -21,6 +21,7 @@ use serde::{Deserialize, Serialize}; use tempfile::TempDir; use tokio::io::AsyncReadExt; +use super::Error as RuntimeError; #[cfg(windows)] use super::startup_ps; #[cfg(unix)] @@ -421,7 +422,7 @@ impl OwnedRuntime { tracing::debug!("cleaning up runtime: {}", &self.name()); match self.0.storage.remove_runtime(self.name()).await { Ok(()) => Ok(()), - Err(Error::UnknownRuntime { .. }) => Ok(()), + Err(Error::Runtime(RuntimeError::UnknownRuntime { .. })) => Ok(()), Err(err) => Err(err), } } @@ -730,7 +731,7 @@ impl Runtime { .collect::>>()?; for entry in walkdir::WalkDir::new(&self.config.upper_dir) { let entry = entry.map_err(|err| { - Error::RuntimeReadError(self.config.upper_dir.clone(), err.into()) + RuntimeError::RuntimeReadError(self.config.upper_dir.clone(), err.into()) })?; let fullpath = entry.path(); if fullpath == self.config.upper_dir { @@ -739,16 +740,20 @@ impl Runtime { for pattern in paths.iter() { let is_dir = entry .metadata() - .map_err(|err| Error::RuntimeReadError(entry.path().to_owned(), err.into()))? + .map_err(|err| { + RuntimeError::RuntimeReadError(entry.path().to_owned(), err.into()) + })? .file_type() .is_dir(); if pattern.is_excluded(fullpath, is_dir) { if is_dir { - std::fs::remove_dir_all(fullpath) - .map_err(|err| Error::RuntimeWriteError(fullpath.to_owned(), err))?; + std::fs::remove_dir_all(fullpath).map_err(|err| { + RuntimeError::RuntimeWriteError(fullpath.to_owned(), err) + })?; } else { - std::fs::remove_file(fullpath) - .map_err(|err| Error::RuntimeWriteError(fullpath.to_owned(), err))?; + std::fs::remove_file(fullpath).map_err(|err| { + RuntimeError::RuntimeWriteError(fullpath.to_owned(), err) + })?; } } } @@ -808,25 +813,27 @@ impl Runtime { &self.config.sh_startup_file, startup_sh::source(environment_overrides_for_child_process), ) - .map_err(|err| Error::RuntimeWriteError(self.config.sh_startup_file.clone(), err))?; + .map_err(|err| RuntimeError::RuntimeWriteError(self.config.sh_startup_file.clone(), err))?; #[cfg(unix)] std::fs::write( &self.config.csh_startup_file, startup_csh::source(environment_overrides_for_child_process), ) - .map_err(|err| Error::RuntimeWriteError(self.config.csh_startup_file.clone(), err))?; + .map_err(|err| { + RuntimeError::RuntimeWriteError(self.config.csh_startup_file.clone(), err) + })?; #[cfg(windows)] std::fs::write( &self.config.ps_startup_file, startup_ps::source(environment_overrides_for_child_process), ) - .map_err(|err| Error::RuntimeWriteError(self.config.ps_startup_file.clone(), err))?; + .map_err(|err| RuntimeError::RuntimeWriteError(self.config.ps_startup_file.clone(), err))?; Ok(()) } async fn ensure_lower_dir(&self) -> Result<()> { if let Err(err) = makedirs_with_perms(&self.config.lower_dir, 0o777) { - return Err(Error::RuntimeWriteError(self.config.lower_dir.clone(), err)); + return Err(RuntimeError::RuntimeWriteError(self.config.lower_dir.clone(), err).into()); } Ok(()) } @@ -835,11 +842,11 @@ impl Runtime { pub async fn ensure_upper_dirs(&self) -> Result<()> { let mut result = makedirs_with_perms(&self.config.upper_dir, 0o777); if let Err(err) = result { - return Err(Error::RuntimeWriteError(self.config.upper_dir.clone(), err)); + return Err(RuntimeError::RuntimeWriteError(self.config.upper_dir.clone(), err).into()); } result = makedirs_with_perms(&self.config.work_dir, 0o777); if let Err(err) = result { - return Err(Error::RuntimeWriteError(self.config.work_dir.clone(), err)); + return Err(RuntimeError::RuntimeWriteError(self.config.work_dir.clone(), err).into()); } Ok(()) } @@ -996,17 +1003,18 @@ impl Storage { /// Access a runtime in this storage /// /// # Errors: - /// - [`Error::UnknownRuntime`] if the named runtime does not exist + /// - [`RuntimeError::UnknownRuntime`] if the named runtime does not exist /// - if there are filesystem errors while reading the runtime on disk pub async fn read_runtime>(&self, name: R) -> Result { let tag_spec = runtime_tag(RuntimeDataType::Metadata, name.as_ref())?; let digest = match self.inner.resolve_tag(&tag_spec).await { Ok(tag) => tag.target, Err(err @ Error::UnknownReference(_)) => { - return Err(Error::UnknownRuntime { + return Err(RuntimeError::UnknownRuntime { runtime: format!("{} in storage {}", name.as_ref(), self.address()), source: Box::new(err), - }); + } + .into()); } Err(err) => return Err(err), }; @@ -1015,17 +1023,17 @@ impl Storage { .open_payload(digest) .await .map_err(|err| match err { - Error::UnknownObject(_) => Error::UnknownRuntime { + Error::UnknownObject(_) => Error::from(RuntimeError::UnknownRuntime { runtime: format!("{} in storage {}", name.as_ref(), self.address()), source: Box::new(err), - }, + }), _ => err, })?; let mut data = String::new(); reader .read_to_string(&mut data) .await - .map_err(|err| Error::RuntimeReadError(filename, err))?; + .map_err(|err| RuntimeError::RuntimeReadError(filename, err))?; let config: Data = serde_json::from_str(&data)?; Ok(Runtime { data: config, @@ -1194,7 +1202,7 @@ impl Storage { upper_root_path.push(name); Ok(upper_root_path) } - _ => Err(Error::DoesNotSupportDurableRuntimePath), + _ => Err(RuntimeError::DoesNotSupportDurableRuntimePath.into()), } } @@ -1214,10 +1222,11 @@ impl Storage { continue; }; if sample_upper_dir == *runtime.upper_dir() { - return Err(Error::RuntimeUpperDirAlreadyInUse { + return Err(RuntimeError::RuntimeUpperDirAlreadyInUse { upper_name, runtime_name: runtime.name().to_string(), - }); + } + .into()); } } Ok(()) @@ -1235,7 +1244,7 @@ impl Storage { let name = name.into(); let runtime_tag = runtime_tag(RuntimeDataType::Metadata, &name)?; match self.inner.resolve_tag(&runtime_tag).await { - Ok(_) => return Err(Error::RuntimeExists(name)), + Ok(_) => return Err(RuntimeError::RuntimeExists(name).into()), Err(Error::UnknownReference(_)) => {} Err(err) => return Err(err), } diff --git a/crates/spfs/src/status.rs b/crates/spfs/src/status.rs index e590154d87..ccd1567c10 100644 --- a/crates/spfs/src/status.rs +++ b/crates/spfs/src/status.rs @@ -9,6 +9,7 @@ mod os; pub use os::*; use super::config::get_config; +use crate::runtime::Error as RuntimeError; use crate::storage::FromConfig; use crate::{Error, Result, runtime, tracking}; @@ -20,13 +21,13 @@ const RUNTIME_REPO_NAME: &str = ""; /// Once modified, active changes can be committed /// /// Errors: -/// - [`Error::NoActiveRuntime`]: if there is no active runtime -/// - [`Error::RuntimeAlreadyEditable`]: if the active runtime is already editable +/// - [`RuntimeError::NoActiveRuntime`]: if there is no active runtime +/// - [`RuntimeError::RuntimeAlreadyEditable`]: if the active runtime is already editable /// - if there are issues remounting the filesystem pub async fn make_active_runtime_editable() -> Result<()> { let mut rt = active_runtime().await?; if rt.status.editable { - return Err(Error::RuntimeAlreadyEditable); + return Err(RuntimeError::RuntimeAlreadyEditable.into()); } rt.status.editable = true; @@ -77,12 +78,12 @@ pub async fn compute_runtime_manifest(rt: &runtime::Runtime) -> Result Result { - let name = std::env::var(SPFS_RUNTIME).map_err(|_| Error::NoActiveRuntime)?; + let name = std::env::var(SPFS_RUNTIME).map_err(|_| RuntimeError::NoActiveRuntime)?; let config = get_config()?; let storage = config.get_runtime_storage().await?; storage.read_runtime(name).await diff --git a/crates/spfs/src/storage/tar/repository.rs b/crates/spfs/src/storage/tar/repository.rs index a17dbc7d49..ff2032db7c 100644 --- a/crates/spfs/src/storage/tar/repository.rs +++ b/crates/spfs/src/storage/tar/repository.rs @@ -16,6 +16,7 @@ use tar::{Archive, Builder}; use crate::config::{ToAddress, pathbuf_deserialize_with_tilde_expansion}; use crate::graph::ObjectProto; use crate::prelude::*; +use crate::runtime::Error as RuntimeError; use crate::storage::fs::DURABLE_EDITS_DIR; use crate::storage::tag::TagSpecAndTagStream; use crate::storage::{ @@ -121,7 +122,7 @@ impl TarRepository { // This will fail if the durable edits directory for runtimes has something in it. tokio::fs::remove_dir(&path) .await - .map_err(|err| Error::RuntimeWriteError(path, err)) + .map_err(|err| Error::from(RuntimeError::RuntimeWriteError(path, err))) } // Open a repository over the given directory, which must already diff --git a/crates/spk-cli/common/src/env.rs b/crates/spk-cli/common/src/env.rs index a223ab24be..cc6c634eb7 100644 --- a/crates/spk-cli/common/src/env.rs +++ b/crates/spk-cli/common/src/env.rs @@ -29,7 +29,7 @@ use crate::Error; /// Load the current environment from the spfs file system. pub async fn current_env() -> crate::Result { let runtime = match spfs::active_runtime().await { - Err(spfs::Error::NoActiveRuntime) => { + Err(spfs::Error::Runtime(spfs::runtime::Error::NoActiveRuntime)) => { return Err(Error::NoEnvironment); } Err(err) => return Err(err.into()), diff --git a/crates/spk-cli/group4/src/cmd_view.rs b/crates/spk-cli/group4/src/cmd_view.rs index 2a45acb419..11cd1f1914 100644 --- a/crates/spk-cli/group4/src/cmd_view.rs +++ b/crates/spk-cli/group4/src/cmd_view.rs @@ -581,7 +581,9 @@ impl View { let mut in_a_runtime = true; let found = match spk_storage::find_path_providers(filepath).await { Ok(f) => f, - Err(spk_storage::Error::SPFS(spfs::Error::NoActiveRuntime)) => { + Err(spk_storage::Error::SPFS(spfs::Error::Runtime( + spfs::runtime::Error::NoActiveRuntime, + ))) => { in_a_runtime = false; Vec::new() } diff --git a/docs/error_codes/_index.md b/docs/error_codes/_index.md index 127f1a3e68..f28c74a1df 100644 --- a/docs/error_codes/_index.md +++ b/docs/error_codes/_index.md @@ -79,6 +79,16 @@ build: This is a generic error code that has no more specific information or help documentation attached. If you encounter one of these, please reach out for help by submitting an issue on [github](https://github.com/spkenv/spk). +### `spfs::runtime` + +This is a category of errors related to SPFS runtime operations including environment initialization, commit operations, and runtime filesystem management. These errors typically occur when: + +- Attempting operations outside an active SPFS runtime +- Runtime initialization or configuration fails +- Filesystem operations within the runtime encounter issues + +If you encounter a runtime error, ensure you are running within an SPFS environment (e.g., via `spfs shell` or `spfs run`). For more details on runtime semantics, see the [runtime documentation]({{< ref "../spfs/develop/runtime" >}}). + ### `spfs::could_not_create_spfs_dir` Spfs relies on a specific directory in which to work. All files in the runtime environment are visible at that location and this root directory must exist before spfs can use it.