Merge branch 'simplify-errorhandling' into 'main'

Simplify errorhandling

See merge request crystal/software/tourmaline!1
main
Julius Riegel 1 year ago
commit eb88743d01

624
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -16,24 +16,25 @@ path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.0.24", features = ["derive"] }
clap = { version = "4.1.6", features = ["derive"] }
color-eyre = "0.6.2"
dotenv = "0.15.0"
embed-nu = "0.3.5"
embed-nu = "0.5.1"
lazy_static = "1.4.0"
libc = "0.2.137"
paste = "1.0.9"
libc = "0.2.139"
miette = { version = "5.5.0", features = ["fancy"] }
paste = "1.0.11"
rusty-value = { version = "0.6.0", features = ["derive", "json"] }
serde = { version = "1.0.147", features = ["derive"] }
serde_json = "1.0.87"
sys-mount = "2.0.1"
thiserror = "1.0.37"
tokio = { version = "1.21.2", features = ["rt", "io-std", "io-util", "process", "time", "macros", "tracing", "fs"] }
toml = "0.5.9"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93"
sys-mount = "2.0.2"
thiserror = "1.0.38"
tokio = { version = "1.25.0", features = ["rt", "io-std", "io-util", "process", "time", "macros", "tracing", "fs"] }
toml = "0.7.2"
tracing = "0.1.37"
tracing-subscriber = "0.3.16"
valico = "3.6.1"
[build-dependencies]
cargo_toml = "0.13.0"
serde = { version = "1.0.147", features = ["derive"] }
cargo_toml = "0.15.2"
serde = { version = "1.0.152", features = ["derive"] }

@ -3,7 +3,8 @@ use std::{collections::HashMap, path::PathBuf};
use serde::Deserialize;
use tokio::fs;
use crate::{error::DistroConfigError, utils::CFG_PATH};
use crate::utils::CFG_PATH;
use miette::{self, Context, IntoDiagnostic, Result};
/// The config file of a distro that defines
/// how that distro should be installed
@ -61,10 +62,13 @@ fn default_order() -> usize {
impl DistroConfig {
#[tracing::instrument(level = "trace", skip_all)]
pub async fn load() -> Result<Self, DistroConfigError> {
pub async fn load() -> Result<Self> {
let path = CFG_PATH.join("distro.toml");
let contents = fs::read_to_string(path).await?;
let cfg = toml::from_str::<Self>(&contents)?;
let contents = fs::read_to_string(path)
.await
.into_diagnostic()
.context("reading config file")?;
let cfg = toml::from_str::<Self>(&contents).into_diagnostic()?;
Ok(cfg)
}

@ -5,9 +5,10 @@ use valico::json_schema::Scope;
use crate::{
distro::distro_config::DistroConfig,
error::{AppResult, OSConfigError, SchemaError},
error::{OSConfigError, SchemaError},
utils::CFG_PATH,
};
use miette::{IntoDiagnostic, Result};
use super::OSConfig;
@ -26,7 +27,7 @@ impl<'a> OSConfigLoader<'a> {
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn load(&self) -> AppResult<OSConfig> {
pub async fn load(&self) -> Result<OSConfig> {
let schema = self.load_extension_schema().await?;
let os_config = OSConfig::load(&self.cfg_path).await?;
Self::validate_config(schema, &os_config)?;
@ -35,7 +36,7 @@ impl<'a> OSConfigLoader<'a> {
}
#[tracing::instrument(level = "trace", skip_all)]
async fn load_extension_schema(&self) -> Result<serde_json::Value, SchemaError> {
async fn load_extension_schema(&self) -> Result<serde_json::Value> {
let schema_path = self
.distro_cfg
.config
@ -43,14 +44,14 @@ impl<'a> OSConfigLoader<'a> {
.as_ref()
.map(|p| CFG_PATH.join(p))
.unwrap_or_else(|| CFG_PATH.join("config.schema.json"));
let contents = fs::read_to_string(schema_path).await?;
let schema = serde_json::from_str(&contents)?;
let contents = fs::read_to_string(schema_path).await.into_diagnostic()?;
let schema = serde_json::from_str(&contents).into_diagnostic()?;
Ok(schema)
}
#[tracing::instrument(level = "trace", skip_all)]
fn validate_config(schema: serde_json::Value, config: &OSConfig) -> AppResult<()> {
fn validate_config(schema: serde_json::Value, config: &OSConfig) -> Result<()> {
let mut scope = Scope::new();
let schema = scope
.compile_and_return(schema, true)

@ -10,10 +10,11 @@ use embed_nu::{
},
RustyIntoValue,
};
use miette::{Context, IntoDiagnostic, Result};
use serde::Deserialize;
use tokio::fs;
use crate::error::{AppResult, OSConfigError};
use crate::error::OSConfigError;
/// Represents the full configuration of the OS including extensions defined
/// by the distro
@ -26,7 +27,7 @@ pub struct OSConfig {
}
impl OSConfig {
pub fn get_nu_value<K: AsRef<str>>(&self, key: K) -> AppResult<embed_nu::Value> {
pub fn get_nu_value<K: AsRef<str>>(&self, key: K) -> Result<embed_nu::Value> {
let value = self.clone().into_rusty_value();
let mut fields = if let Value::Struct(Struct { fields, .. }) = value {
if let Fields::Named(named) = fields {
@ -114,9 +115,14 @@ fn json_to_rusty_value(val: serde_json::Value) -> embed_nu::rusty_value::Value {
}
impl OSConfig {
pub(crate) async fn load(path: &Path) -> Result<Self, OSConfigError> {
let contents = fs::read_to_string(path).await?;
let cfg = serde_json::from_str::<Self>(&contents)?;
pub(crate) async fn load(path: &Path) -> Result<Self> {
let contents = fs::read_to_string(path)
.await
.into_diagnostic()
.context("reading config contents")?;
let cfg = serde_json::from_str::<Self>(&contents)
.into_diagnostic()
.context("parsing_json")?;
Ok(cfg)
}

@ -1,5 +1,6 @@
use std::{io, path::PathBuf};
use miette::Diagnostic;
use thiserror::Error;
pub type AppResult<T> = std::result::Result<T, AppError>;
@ -31,18 +32,18 @@ pub enum AppError {
Chroot(#[from] ChrootError),
}
#[derive(Error, Debug)]
#[derive(Error, Debug, Diagnostic)]
pub enum ScriptError {
#[error("IO Error when trying to read script file: {0}")]
#[diagnostic(code(trm::script::io_error))]
Io(#[from] io::Error),
#[error("Could not find the script file at {0}")]
#[diagnostic(code(trm::script::not_found))]
ScriptNotFound(PathBuf),
#[error("Nu error when executing script: {0}")]
NuError(#[from] embed_nu::Error),
#[error("Could not find the main method in the script file: {0}")]
#[diagnostic(code(trm::script::no_main))]
MissingMain(PathBuf),
}
@ -55,59 +56,75 @@ pub enum DistroConfigError {
InvalidToml(#[from] toml::de::Error),
}
#[derive(Error, Debug)]
#[derive(Error, Debug, Diagnostic)]
pub enum SchemaError {
#[diagnostic(code(trm::schema::io))]
#[error("IO Error when trying to read json-schema file: {0}")]
Io(#[from] io::Error),
#[error("Encountered invalid JSON when parsing json-schema: {0}")]
#[diagnostic(code(trm::schema::invalid_json))]
InvalidJson(#[from] serde_json::Error),
#[error("Failed to parse the json-schema: {0}")]
#[diagnostic(code(trm::schema::invalid_schema))]
ParseSchema(#[from] valico::json_schema::SchemaError),
}
#[derive(Error, Debug)]
#[derive(Error, Debug, Diagnostic)]
pub enum OSConfigError {
#[diagnostic(code(trm::os_config::io))]
#[error("IO Error when trying to read OSConfig file: {0}")]
Io(#[from] io::Error),
#[diagnostic(code(trm::os_config::invalid_json))]
#[error("Encountered invalid JSON when parsing OSConfig: {0}")]
InvalidJson(#[from] serde_json::Error),
#[diagnostic(code(trm::os_config::invalid))]
#[error("The os config is invalid:\n{0}")]
Validation(String),
#[diagnostic(code(trm::os_config::missing_key))]
#[error("Missing config key {0}")]
MissingConfigKey(String),
}
#[derive(Error, Debug)]
#[derive(Error, Debug, Diagnostic)]
pub enum ChrootError {
#[diagnostic(code(trm::chroot::not_found))]
#[error("Could not find chroot directory {0}")]
NotFound(PathBuf),
#[error("Failed to unshare FS resources with parent: {0}")]
#[diagnostic(code(trm::chroot::unshare))]
Unshare(io::Error),
#[error("Failed to create chroot dir: {0}")]
#[diagnostic(code(trm::chroot::create))]
CreateChroot(io::Error),
#[error("Failed to chroot: {0}")]
#[diagnostic(code(trm::chroot::enter))]
Chroot(io::Error),
#[error("Failed to mount directory {0} in chroot: {1}")]
#[diagnostic(code(trm::chroot::mount))]
Mount(PathBuf, io::Error),
#[error("Failed to create symlink {0} in chroot: {1}")]
#[error("Failed to create symlink to {0} in chroot: {1}")]
#[diagnostic(code(trm::chroot::symlink))]
Link(PathBuf, io::Error),
#[error("Failed to remove symlink in chroot: {0}")]
#[diagnostic(code(trm::chroot::unlink))]
Unlink(io::Error),
#[error("Failed to copy file {0} to chroot: {1}")]
#[diagnostic(code(trm::chroot::copy_file))]
Copy(PathBuf, io::Error),
#[error("Failed to change process working directory: {0}")]
#[diagnostic(code(trm::chroot::chdir))]
ChDir(io::Error),
}

@ -4,15 +4,15 @@ pub(crate) mod utils;
use std::path::PathBuf;
use distro::{distro_config::DistroConfig, loader::OSConfigLoader};
use error::AppResult;
use task::task_executor::TaskExecutor;
pub use utils::generate_script_files;
pub mod distro;
pub mod task;
use miette::Result;
/// Creates a new executor with the given os config for the current distro
#[tracing::instrument(level = "trace")]
pub async fn create_executor(os_cfg_path: PathBuf) -> AppResult<TaskExecutor> {
pub async fn create_executor(os_cfg_path: PathBuf) -> Result<TaskExecutor> {
let distro_config = DistroConfig::load().await?;
let os_config = OSConfigLoader::new(os_cfg_path, &distro_config)
.load()

@ -1,15 +1,17 @@
use args::{Args, Command, CreateEmptyConfigArgs, GenerateScriptsArgs, InstallFromConfigArgs};
use clap::Parser;
use miette::{Context, IntoDiagnostic, Result};
use rusty_value::into_json::{EnumRepr, IntoJson, IntoJsonOptions};
use tokio::fs;
use tourmaline::{distro::OSConfig, error::AppResult, generate_script_files};
use tourmaline::{distro::OSConfig, generate_script_files};
mod args;
#[tokio::main(flavor = "current_thread")]
async fn main() -> color_eyre::Result<()> {
async fn main() -> miette::Result<()> {
miette::set_panic_hook();
color_eyre::install().unwrap();
dotenv::dotenv().unwrap();
let _ = dotenv::dotenv();
let args = Args::parse();
match args.command {
@ -22,7 +24,7 @@ async fn main() -> color_eyre::Result<()> {
}
/// Installs the distro from a given configuration file
async fn install_from_config(args: InstallFromConfigArgs) -> AppResult<()> {
async fn install_from_config(args: InstallFromConfigArgs) -> Result<()> {
tourmaline::create_executor(args.path)
.await?
.with_base_tasks()
@ -31,16 +33,21 @@ async fn install_from_config(args: InstallFromConfigArgs) -> AppResult<()> {
.await
}
async fn generate_scripts(args: GenerateScriptsArgs) -> AppResult<()> {
async fn generate_scripts(args: GenerateScriptsArgs) -> Result<()> {
generate_script_files(args.path).await
}
async fn generate_empty_config(args: CreateEmptyConfigArgs) -> AppResult<()> {
async fn generate_empty_config(args: CreateEmptyConfigArgs) -> Result<()> {
let config = OSConfig::empty().into_json_with_options(&IntoJsonOptions {
enum_repr: EnumRepr::Untagged,
});
let config_string = serde_json::to_string_pretty(&config)?;
fs::write(args.path, config_string).await?;
let config_string = serde_json::to_string_pretty(&config)
.into_diagnostic()
.context("serializing default config")?;
fs::write(args.path, config_string)
.await
.into_diagnostic()
.context("writing empty config")?;
Ok(())
}

@ -1,10 +1,11 @@
use std::path::PathBuf;
use crate::{distro::OSConfig, error::AppResult};
use crate::distro::OSConfig;
use super::{exec_builder::ExecBuilder, TaskTrait};
use embed_nu::IntoValue;
use lazy_static::lazy_static;
use miette::Result;
#[derive(Clone, Debug)]
pub enum BaseTask {
@ -75,7 +76,7 @@ impl BaseTask {
impl TaskTrait for BaseTask {
#[tracing::instrument(level = "trace", skip_all)]
fn up(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
fn up(&self, config: &OSConfig) -> Result<Option<ExecBuilder>> {
let key_data = self.key_data();
let script = PathBuf::from(key_data.task_name).join("up.nu");
@ -90,7 +91,7 @@ impl TaskTrait for BaseTask {
}
#[tracing::instrument(level = "trace", skip_all)]
fn down(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
fn down(&self, config: &OSConfig) -> Result<Option<ExecBuilder>> {
let key_data = self.key_data();
let script = PathBuf::from(key_data.task_name).join("down.nu");
let task_config = if let Some(key) = key_data.config_key {

@ -142,9 +142,9 @@ impl Mapping {
}
fs::symlink(src, dst)
.await
.map_err(|e| ChrootError::Link(dst.to_owned(), e))?;
.map_err(|e| ChrootError::Link(src.to_owned(), e))?;
Ok(MappingHandle::Link(LinkDrop {
path: dst.to_owned(),
path: src.to_owned(),
}))
}
@ -152,12 +152,12 @@ impl Mapping {
if dst.exists() && dst.is_file() {
fs::remove_file(dst)
.await
.map_err(|e| ChrootError::Copy(dst.to_owned(), e))?;
.map_err(|e| ChrootError::Copy(src.to_owned(), e))?;
}
fs::copy(src, dst)
.await
.map_err(|e| ChrootError::Copy(dst.to_owned(), e))?;
.map_err(|e| ChrootError::Copy(src.to_owned(), e))?;
Ok(MappingHandle::None)
}

@ -5,10 +5,11 @@ use tokio::fs;
use crate::error::ChrootError;
use super::{mapping::default_mappings, Chroot};
use miette::Result;
impl Chroot {
/// Creates a new chroot with the given path
pub async fn create<P: Into<PathBuf>>(root_path: P) -> Result<Self, ChrootError> {
pub async fn create<P: Into<PathBuf>>(root_path: P) -> Result<Self> {
let root_path = root_path.into();
if !root_path.exists() {
fs::create_dir_all(&root_path)

@ -2,9 +2,10 @@ use std::path::PathBuf;
use embed_nu::IntoValue;
use crate::{distro::OSConfig, error::AppResult};
use crate::distro::OSConfig;
use super::{exec_builder::ExecBuilder, TaskTrait};
use miette::Result;
#[derive(Clone, Debug)]
pub struct CustomTask {
@ -35,7 +36,7 @@ impl CustomTask {
impl TaskTrait for CustomTask {
#[tracing::instrument(level = "trace", skip_all)]
fn up(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
fn up(&self, config: &OSConfig) -> Result<Option<ExecBuilder>> {
let task_config = if let Some(key) = self.config_key.as_ref() {
config.get_nu_value(key)?
} else {
@ -52,7 +53,7 @@ impl TaskTrait for CustomTask {
}
#[tracing::instrument(level = "trace", skip_all)]
fn down(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
fn down(&self, config: &OSConfig) -> Result<Option<ExecBuilder>> {
let task_config = if let Some(key) = self.config_key.as_ref() {
config.get_nu_value(key)?
} else {

@ -1,14 +1,15 @@
use std::path::{Path, PathBuf};
use embed_nu::{Argument, CommandGroupConfig, Context, ValueIntoExpression};
use embed_nu::{Argument, CommandGroupConfig, ValueIntoExpression};
use std::fs;
use crate::{distro::OSConfig, error::ScriptError, utils::CFG_PATH};
use miette::{Context, IntoDiagnostic, Result};
#[derive(Clone)]
pub struct ExecBuilder {
task_config: embed_nu::Value,
ctx: Context,
ctx: embed_nu::Context,
}
impl ExecBuilder {
@ -16,29 +17,36 @@ impl ExecBuilder {
script: PathBuf,
os_config: OSConfig,
task_config: embed_nu::Value,
) -> Result<Self, ScriptError> {
) -> Result<Self> {
let script_contents = Self::get_script_contents(&script)?;
let mut ctx = Context::builder()
.with_command_groups(CommandGroupConfig::default().all_groups(true))?
let mut ctx = embed_nu::Context::builder()
.with_command_groups(CommandGroupConfig::default().all_groups(true))
.into_diagnostic()?
.add_parent_env_vars()
.add_var("TRM_CONFIG", os_config)?
.add_script(script_contents)?
.build()?;
.add_var("TRM_CONFIG", os_config)
.into_diagnostic()?
.add_script(script_contents)
.into_diagnostic()?
.build()
.into_diagnostic()?;
if !ctx.has_fn("main") {
Err(ScriptError::MissingMain(script))
Err(ScriptError::MissingMain(script).into())
} else {
Ok(Self { ctx, task_config })
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn exec(mut self) -> Result<(), ScriptError> {
let pipeline = self.ctx.call_fn(
"main",
vec![Argument::Positional(self.task_config.into_expression())],
)?;
self.ctx.print_pipeline_stderr(pipeline)?;
pub fn exec(mut self) -> Result<()> {
let pipeline = self
.ctx
.call_fn(
"main",
vec![Argument::Positional(self.task_config.into_expression())],
)
.into_diagnostic()?;
self.ctx.print_pipeline_stderr(pipeline).into_diagnostic()?;
Ok(())
}
@ -52,13 +60,16 @@ impl ExecBuilder {
}
#[tracing::instrument(level = "trace", skip_all)]
fn get_script_contents(path: &Path) -> Result<String, ScriptError> {
fn get_script_contents(path: &Path) -> Result<String> {
let path = CFG_PATH.join(path);
if path.exists() {
fs::read_to_string(path).map_err(ScriptError::from)
fs::read_to_string(path)
.map_err(ScriptError::from)
.into_diagnostic()
.context("reading script contents")
} else {
Err(ScriptError::ScriptNotFound(path))
Err(ScriptError::ScriptNotFound(path).into())
}
}
}

@ -1,6 +1,6 @@
use std::cmp::Ordering;
use crate::{distro::OSConfig, error::AppResult};
use crate::distro::OSConfig;
use self::{base_task::BaseTask, custom_task::CustomTask, exec_builder::ExecBuilder};
pub mod base_task;
@ -8,10 +8,11 @@ mod chrooting;
pub mod custom_task;
pub mod exec_builder;
pub mod task_executor;
use miette::Result;
pub trait TaskTrait {
fn up(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>>;
fn down(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>>;
fn up(&self, config: &OSConfig) -> Result<Option<ExecBuilder>>;
fn down(&self, config: &OSConfig) -> Result<Option<ExecBuilder>>;
/// Used to decide the execution order
/// smaller values mean the task get's executed earlier
fn order(&self) -> usize;
@ -50,7 +51,7 @@ impl Task {
impl TaskTrait for Task {
#[inline]
fn up(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
fn up(&self, config: &OSConfig) -> Result<Option<ExecBuilder>> {
match self {
Task::Base(b) => b.up(config),
Task::Custom(c) => c.up(config),
@ -58,7 +59,7 @@ impl TaskTrait for Task {
}
#[inline]
fn down(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
fn down(&self, config: &OSConfig) -> Result<Option<ExecBuilder>> {
match self {
Task::Base(b) => b.down(config),
Task::Custom(c) => c.down(config),

@ -1,6 +1,5 @@
use crate::{
distro::{distro_config::DistroConfig, OSConfig},
error::AppResult,
utils::ROOT_MNT,
};
@ -13,6 +12,7 @@ pub struct TaskExecutor {
os_config: OSConfig,
tasks: Vec<Task>,
}
use miette::Result;
impl TaskExecutor {
/// Creates a new task executor with the given OSConfig and Distro Config
@ -61,7 +61,7 @@ impl TaskExecutor {
/// Executes all tasks
#[tracing::instrument(level = "trace", skip_all)]
pub async fn execute(&mut self) -> AppResult<()> {
pub async fn execute(&mut self) -> Result<()> {
self.tasks.sort_by(Task::compare);
let chroot = Chroot::create(&*ROOT_MNT).await?;

@ -3,8 +3,8 @@ use std::{env, path::PathBuf};
use tokio::fs;
use crate::error::AppResult;
use crate::task::base_task::ALL_BASE_TASKS;
use miette::{Context, IntoDiagnostic, Result};
const DEFAULT_CONFIG_DIR: &str = "/etc";
@ -19,7 +19,7 @@ macro_rules! env_cfg {
env_cfg!(CFG_PATH: PathBuf <- "TRM_CFG_PATH" default PathBuf::from(DEFAULT_CONFIG_DIR).join("tourmaline"));
env_cfg!(ROOT_MNT: PathBuf <- "TRM_ROOT_MNT" default PathBuf::from("/mnt"));
pub async fn generate_script_files<P: AsRef<Path>>(output: P) -> AppResult<()> {
pub async fn generate_script_files<P: AsRef<Path>>(output: P) -> Result<()> {
let output = output.as_ref();
for task in &*ALL_BASE_TASKS {
@ -28,7 +28,10 @@ pub async fn generate_script_files<P: AsRef<Path>>(output: P) -> AppResult<()> {
let script_dir = output.join(name);
if !script_dir.exists() {
fs::create_dir_all(&script_dir).await?;
fs::create_dir_all(&script_dir)
.await
.into_diagnostic()
.context("creating script dir")?;
}
let up_path = output.join("up.nu");
let down_path = output.join("down.nu");
@ -43,7 +46,9 @@ def main [cfg] {{
"#
),
)
.await?;
.await
.into_diagnostic()
.context("writing up task content")?;
fs::write(
&down_path,
@ -56,7 +61,9 @@ def main [cfg] {{
)
.trim(),
)
.await?;
.await
.into_diagnostic()
.context("writing down task content")?;
}
Ok(())

Loading…
Cancel
Save