Improve error reporting

config-extension
trivernis 2 years ago
parent 618c1ddf9f
commit d50cbf4ca1
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

1
Cargo.lock generated

@ -3523,6 +3523,7 @@ dependencies = [
"serde_json", "serde_json",
"thiserror", "thiserror",
"tokio", "tokio",
"toml",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"valico", "valico",

@ -27,6 +27,7 @@ serde = { version = "1.0.145", features = ["derive"] }
serde_json = "1.0.86" serde_json = "1.0.86"
thiserror = "1.0.37" thiserror = "1.0.37"
tokio = { version = "1.21.2", features = ["rt", "io-std", "io-util", "process", "time", "macros", "tracing", "fs"] } tokio = { version = "1.21.2", features = ["rt", "io-std", "io-util", "process", "time", "macros", "tracing", "fs"] }
toml = "0.5.9"
tracing = "0.1.37" tracing = "0.1.37"
tracing-subscriber = "0.3.16" tracing-subscriber = "0.3.16"
valico = "3.6.1" valico = "3.6.1"

@ -1,6 +1,7 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://getcryst.al/config.schema.json", "$id": "https://getcryst.al/config.schema.json",
"type": "object",
"properties": { "properties": {
"enable_flatpak": { "enable_flatpak": {
"type": "boolean" "type": "boolean"
@ -10,6 +11,35 @@
}, },
"enable_zramd": { "enable_zramd": {
"type": "boolean" "type": "boolean"
},
"unakite": {
"type": "object",
"properties": {
"root": {
"type": "string"
},
"old_root": {
"type": "string"
},
"efidir": {
"type": "string"
},
"bootdev": {
"type": "string"
} }
},
"required": [
"root",
"old_root",
"efidir",
"bootdev"
]
} }
},
"required": [
"enable_flatpak",
"enable_timeshift",
"enable_zramd",
"unakite"
]
} }

@ -7,11 +7,17 @@ schema = "config.schema.json"
[tasks] [tasks]
[tasks.enable_flatpak] [tasks.install-flatpak]
config_field = "enable_flatpak" config_key = "enable_flatpak"
skip_on_false = true
[tasks.enable_timeshift] [tasks.install-timeshift]
config_field = "enable_timeshift" config_key = "enable_timeshift"
skip_on_false = true
[tasks.enable_zramd] [tasks.install-zramd]
config_field = "enable_zramd" config_key = "enable_zramd"
skip_on_false = true
[tasks.configure-unakite]
config_key = "unakite"

@ -1,4 +1,5 @@
# Applies all system changes of `setup-users` # Applies all system changes of `setup-users`
def main [cfg] { def main [cfg] {
echo "Executing up task `setup-users` with config" $cfg echo "Executing up task `setup-users` with config" $cfg
echo $TRM_CONFIG
} }

@ -30,7 +30,7 @@ pub enum Command {
/// *For testing purposes only* /// *For testing purposes only*
/// Generates the JSON for an empty config file /// Generates the JSON for an empty config file
#[command()] #[command()]
CreateEmptyConfig(GenerateEmptyConfigArgs), CreateEmptyConfig(CreateEmptyConfigArgs),
} }
#[derive(Debug, Clone, Parser)] #[derive(Debug, Clone, Parser)]
@ -48,7 +48,7 @@ pub struct GenerateScriptsArgs {
} }
#[derive(Debug, Clone, Parser)] #[derive(Debug, Clone, Parser)]
pub struct GenerateEmptyConfigArgs { pub struct CreateEmptyConfigArgs {
/// The path to the empty configuration file /// The path to the empty configuration file
#[arg(default_value = "config.json")] #[arg(default_value = "config.json")]
pub path: PathBuf, pub path: PathBuf,

@ -3,7 +3,7 @@ use std::{collections::HashMap, path::PathBuf};
use serde::Deserialize; use serde::Deserialize;
use tokio::fs; use tokio::fs;
use crate::{error::AppResult, utils::CFG_PATH}; use crate::{error::DistroConfigError, utils::CFG_PATH};
/// The config file of a distro that defines /// The config file of a distro that defines
/// how that distro should be installed /// how that distro should be installed
@ -43,14 +43,16 @@ pub struct TaskConfig {
/// If the task should be skipped if the /// If the task should be skipped if the
/// config value of that task is null /// config value of that task is null
#[serde(default)]
pub skip_on_false: bool, pub skip_on_false: bool,
} }
impl DistroConfig { impl DistroConfig {
pub async fn load() -> AppResult<Self> { #[tracing::instrument(level = "trace", skip_all)]
pub async fn load() -> Result<Self, DistroConfigError> {
let path = CFG_PATH.join("distro.toml"); let path = CFG_PATH.join("distro.toml");
let contents = fs::read_to_string(path).await?; let contents = fs::read_to_string(path).await?;
let cfg = serde_json::from_str::<Self>(&contents)?; let cfg = toml::from_str::<Self>(&contents)?;
Ok(cfg) Ok(cfg)
} }

@ -46,7 +46,7 @@ impl BaseConfig {
additional: Vec::new(), additional: Vec::new(),
}, },
desktop: DesktopConfig::KdePlasma, desktop: DesktopConfig::KdePlasma,
users: UsersConfig { users: Vec::new() }, users: UsersConfig(Vec::new()),
root_user: RootUserConfig { root_user: RootUserConfig {
password: String::new(), password: String::new(),
}, },
@ -151,9 +151,7 @@ pub struct RootUserConfig {
} }
#[derive(Clone, Debug, Deserialize, RustyValue)] #[derive(Clone, Debug, Deserialize, RustyValue)]
pub struct UsersConfig { pub struct UsersConfig(Vec<User>);
pub users: Vec<User>,
}
#[derive(Clone, Debug, Deserialize, RustyValue)] #[derive(Clone, Debug, Deserialize, RustyValue)]
pub struct User { pub struct User {

@ -5,12 +5,13 @@ use valico::json_schema::Scope;
use crate::{ use crate::{
distro::distro_config::DistroConfig, distro::distro_config::DistroConfig,
error::{AppError, AppResult}, error::{AppResult, OSConfigError, SchemaError},
utils::CFG_PATH, utils::CFG_PATH,
}; };
use super::OSConfig; use super::OSConfig;
#[derive(Debug)]
pub struct OSConfigLoader<'a> { pub struct OSConfigLoader<'a> {
distro_cfg: &'a DistroConfig, distro_cfg: &'a DistroConfig,
cfg_path: PathBuf, cfg_path: PathBuf,
@ -24,6 +25,7 @@ impl<'a> OSConfigLoader<'a> {
} }
} }
#[tracing::instrument(level = "trace", skip_all)]
pub async fn load(&self) -> AppResult<OSConfig> { pub async fn load(&self) -> AppResult<OSConfig> {
let schema = self.load_extension_schema().await?; let schema = self.load_extension_schema().await?;
let os_config = OSConfig::load(&self.cfg_path).await?; let os_config = OSConfig::load(&self.cfg_path).await?;
@ -32,13 +34,14 @@ impl<'a> OSConfigLoader<'a> {
Ok(os_config) Ok(os_config)
} }
async fn load_extension_schema(&self) -> AppResult<serde_json::Value> { #[tracing::instrument(level = "trace", skip_all)]
async fn load_extension_schema(&self) -> Result<serde_json::Value, SchemaError> {
let schema_path = self let schema_path = self
.distro_cfg .distro_cfg
.config .config
.schema .schema
.as_ref() .as_ref()
.map(PathBuf::from) .map(|p| CFG_PATH.join(p))
.unwrap_or_else(|| CFG_PATH.join("config.schema.json")); .unwrap_or_else(|| CFG_PATH.join("config.schema.json"));
let contents = fs::read_to_string(schema_path).await?; let contents = fs::read_to_string(schema_path).await?;
let schema = serde_json::from_str(&contents)?; let schema = serde_json::from_str(&contents)?;
@ -46,33 +49,29 @@ impl<'a> OSConfigLoader<'a> {
Ok(schema) 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) -> AppResult<()> {
let mut scope = Scope::new(); let mut scope = Scope::new();
let schema = scope.compile_and_return(schema, true)?; let schema = scope
let mut errors = Vec::new(); .compile_and_return(schema, true)
.map_err(SchemaError::ParseSchema)?;
let ext_value = serde_json::Value::Object(config.extended.clone().into_iter().collect());
for (key, value) in config.extended.iter() { let result = schema.validate(&ext_value);
let result = schema.validate_in(value, key);
for error in result.errors { if result.is_valid() {
tracing::error!( tracing::debug!("Config is valid");
"ConfigError: {} ({}) at {}",
error.get_title(),
error.get_code(),
error.get_path(),
);
errors.push(error);
}
}
if errors.is_empty() {
Ok(()) Ok(())
} else { } else {
let msg = errors let msg = result
.errors
.into_iter() .into_iter()
.map(|e| format!("{} ({}) at {}", e.get_title(), e.get_code(), e.get_path())) .map(|e| format!("{} > {}", e.get_path(), e.get_title()))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n"); .join("\n");
Err(AppError::InvalidConfig(msg)) tracing::error!("Config is invalid");
Err(OSConfigError::Validation(msg).into())
} }
} }
} }

@ -13,7 +13,7 @@ use embed_nu::{
use serde::Deserialize; use serde::Deserialize;
use tokio::fs; use tokio::fs;
use crate::error::{AppError, AppResult}; use crate::error::{AppResult, OSConfigError};
/// Represents the full configuration of the OS including extensions defined /// Represents the full configuration of the OS including extensions defined
/// by the distro /// by the distro
@ -40,7 +40,7 @@ impl OSConfig {
fields fields
.remove(key.as_ref()) .remove(key.as_ref())
.map(|v| v.into_value()) .map(|v| v.into_value())
.ok_or_else(|| AppError::MissingConfigKey(key.as_ref().to_owned())) .ok_or_else(|| OSConfigError::MissingConfigKey(key.as_ref().to_owned()).into())
} }
} }
@ -114,7 +114,7 @@ fn json_to_rusty_value(val: serde_json::Value) -> embed_nu::rusty_value::Value {
} }
impl OSConfig { impl OSConfig {
pub(crate) async fn load(path: &Path) -> AppResult<Self> { pub(crate) async fn load(path: &Path) -> Result<Self, OSConfigError> {
let contents = fs::read_to_string(path).await?; let contents = fs::read_to_string(path).await?;
let cfg = serde_json::from_str::<Self>(&contents)?; let cfg = serde_json::from_str::<Self>(&contents)?;

@ -6,36 +6,75 @@ pub type AppResult<T> = std::result::Result<T, AppError>;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum AppError { pub enum AppError {
#[error("Could not find the script file")] #[error("Missing config")]
MissingConfig,
#[error("IO Error: {0}")]
Io(#[from] io::Error),
#[error("JSON deserialization error {0}")]
JSON(#[from] serde_json::Error),
#[error(transparent)]
Script(#[from] ScriptError),
#[error(transparent)]
DistroConfig(#[from] DistroConfigError),
#[error(transparent)]
Schema(#[from] SchemaError),
#[error(transparent)]
OSConfig(#[from] OSConfigError),
}
#[derive(Error, Debug)]
pub enum ScriptError {
#[error("IO Error when trying to read script file: {0}")]
Io(#[from] io::Error),
#[error("Could not find the script file at {0}")]
ScriptNotFound(PathBuf), ScriptNotFound(PathBuf),
#[error("Nu error {0}")] #[error("Nu error when executing script: {0}")]
NuError(#[from] embed_nu::Error), NuError(#[from] embed_nu::Error),
#[error("Could not find the main mehod in the script file {0}")] #[error("Could not find the main method in the script file: {0}")]
MissingMain(PathBuf), MissingMain(PathBuf),
}
#[error("Failed to execute script")] #[derive(Error, Debug)]
FailedToExecuteScript, pub enum DistroConfigError {
#[error("IO Error when trying to read distro config: {0}")]
Io(#[from] io::Error),
#[error("Missing config")] #[error("Encountered invalid Toml when parsing distro config: {0}")]
MissingConfig, InvalidToml(#[from] toml::de::Error),
}
#[error("Missing config key {0}")] #[derive(Error, Debug)]
MissingConfigKey(String), pub enum SchemaError {
#[error("IO Error when trying to read json-schema file: {0}")]
Io(#[from] io::Error),
#[error("The os config is invalid: {0}")] #[error("Encountered invalid JSON when parsing json-schema: {0}")]
InvalidConfig(String), InvalidJson(#[from] serde_json::Error),
#[error("IO Error: {0}")] #[error("Failed to parse the json-schema: {0}")]
ParseSchema(#[from] valico::json_schema::SchemaError),
}
#[derive(Error, Debug)]
pub enum OSConfigError {
#[error("IO Error when trying to read OSConfig file: {0}")]
Io(#[from] io::Error), Io(#[from] io::Error),
#[error("JSON deserialization error {0}")] #[error("Encountered invalid JSON when parsing OSConfig: {0}")]
JSON(#[from] serde_json::Error), InvalidJson(#[from] serde_json::Error),
#[error("The task has been skipped")] #[error("The os config is invalid:\n{0}")]
Skipped, Validation(String),
#[error("Failed to process config schema: {0}")] #[error("Missing config key {0}")]
ConfigSchema(#[from] valico::json_schema::SchemaError), MissingConfigKey(String),
} }

@ -11,6 +11,7 @@ pub mod distro;
pub mod task; pub mod task;
/// Creates a new executor with the given os config for the current distro /// 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) -> AppResult<TaskExecutor> {
let distro_config = DistroConfig::load().await?; let distro_config = DistroConfig::load().await?;
let os_config = OSConfigLoader::new(os_cfg_path, &distro_config) let os_config = OSConfigLoader::new(os_cfg_path, &distro_config)

@ -1,4 +1,4 @@
use args::{Args, Command, GenerateEmptyConfigArgs, GenerateScriptsArgs, InstallFromConfigArgs}; use args::{Args, Command, CreateEmptyConfigArgs, GenerateScriptsArgs, InstallFromConfigArgs};
use clap::Parser; use clap::Parser;
use rusty_value::into_json::{EnumRepr, IntoJson, IntoJsonOptions}; use rusty_value::into_json::{EnumRepr, IntoJson, IntoJsonOptions};
use tokio::fs; use tokio::fs;
@ -7,7 +7,7 @@ use tourmaline::{distro::OSConfig, error::AppResult, generate_script_files};
mod args; mod args;
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn main() { async fn main() -> color_eyre::Result<()> {
color_eyre::install().unwrap(); color_eyre::install().unwrap();
dotenv::dotenv().unwrap(); dotenv::dotenv().unwrap();
let args = Args::parse(); let args = Args::parse();
@ -16,8 +16,9 @@ async fn main() {
Command::InstallFromConfig(args) => install_from_config(args).await, Command::InstallFromConfig(args) => install_from_config(args).await,
Command::GenerateScripts(args) => generate_scripts(args).await, Command::GenerateScripts(args) => generate_scripts(args).await,
Command::CreateEmptyConfig(args) => generate_empty_config(args).await, Command::CreateEmptyConfig(args) => generate_empty_config(args).await,
} }?;
.unwrap();
Ok(())
} }
/// Installs the distro from a given configuration file /// Installs the distro from a given configuration file
@ -34,7 +35,7 @@ async fn generate_scripts(args: GenerateScriptsArgs) -> AppResult<()> {
generate_script_files(args.path).await generate_script_files(args.path).await
} }
async fn generate_empty_config(args: GenerateEmptyConfigArgs) -> AppResult<()> { async fn generate_empty_config(args: CreateEmptyConfigArgs) -> AppResult<()> {
let config = OSConfig::empty().into_json_with_options(&IntoJsonOptions { let config = OSConfig::empty().into_json_with_options(&IntoJsonOptions {
enum_repr: EnumRepr::Untagged, enum_repr: EnumRepr::Untagged,
}); });

@ -69,6 +69,7 @@ impl BaseTask {
} }
impl TaskTrait for BaseTask { impl TaskTrait for BaseTask {
#[tracing::instrument(level = "trace", skip_all)]
fn up(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> { fn up(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
let key_data = self.key_data(); let key_data = self.key_data();
let script = PathBuf::from(key_data.task_name).join("up.nu"); let script = PathBuf::from(key_data.task_name).join("up.nu");
@ -86,6 +87,7 @@ impl TaskTrait for BaseTask {
})) }))
} }
#[tracing::instrument(level = "trace", skip_all)]
fn down(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> { fn down(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
let key_data = self.key_data(); let key_data = self.key_data();
let script = PathBuf::from(key_data.task_name).join("down.nu"); let script = PathBuf::from(key_data.task_name).join("down.nu");

@ -25,10 +25,11 @@ impl CustomTask {
} }
impl TaskTrait for CustomTask { impl TaskTrait for CustomTask {
#[tracing::instrument(level = "trace", skip_all)]
fn up(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> { fn up(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
let task_config = config.get_nu_value(&self.config_key)?; let task_config = config.get_nu_value(&self.config_key)?;
if self.skip_on_false && task_config.is_nothing() { if self.skip_on_false && config_is_falsy(&task_config) {
Ok(None) Ok(None)
} else { } else {
Ok(Some(ExecBuilder { Ok(Some(ExecBuilder {
@ -39,10 +40,11 @@ impl TaskTrait for CustomTask {
} }
} }
#[tracing::instrument(level = "trace", skip_all)]
fn down(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> { fn down(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
let task_config = config.get_nu_value(&self.config_key)?; let task_config = config.get_nu_value(&self.config_key)?;
if self.skip_on_false && task_config.is_nothing() { if self.skip_on_false && config_is_falsy(&task_config) {
Ok(None) Ok(None)
} else { } else {
Ok(Some(ExecBuilder { Ok(Some(ExecBuilder {
@ -53,3 +55,12 @@ impl TaskTrait for CustomTask {
} }
} }
} }
fn config_is_falsy(config: &embed_nu::Value) -> bool {
if config.is_nothing() {
return true;
} else if let Ok(b) = config.as_bool() {
return !b;
}
return false;
}

@ -3,10 +3,7 @@ use std::path::PathBuf;
use embed_nu::{Argument, CommandGroupConfig, Context, ValueIntoExpression}; use embed_nu::{Argument, CommandGroupConfig, Context, ValueIntoExpression};
use tokio::fs; use tokio::fs;
use crate::{ use crate::{distro::OSConfig, error::ScriptError, utils::CFG_PATH};
distro::OSConfig,
error::{AppError, AppResult},
};
pub struct ExecBuilder { pub struct ExecBuilder {
pub script: PathBuf, pub script: PathBuf,
@ -15,13 +12,15 @@ pub struct ExecBuilder {
} }
impl ExecBuilder { impl ExecBuilder {
pub async fn exec(self) -> AppResult<()> { #[tracing::instrument(level = "trace", skip_all)]
pub async fn exec(self) -> Result<(), ScriptError> {
let script_contents = self.get_script_contents().await?; let script_contents = self.get_script_contents().await?;
let mut ctx = Context::builder() let mut ctx = Context::builder()
.with_command_groups(CommandGroupConfig::default().all_groups(true))? .with_command_groups(CommandGroupConfig::default().all_groups(true))?
.add_var("TRM_CONFIG", self.os_config)? .add_var("TRM_CONFIG", self.os_config)?
.add_script(script_contents)? .add_script(script_contents)?
.build()?; .build()?;
if ctx.has_fn("main") { if ctx.has_fn("main") {
let pipeline = ctx.call_fn( let pipeline = ctx.call_fn(
"main", "main",
@ -31,13 +30,18 @@ impl ExecBuilder {
Ok(()) Ok(())
} else { } else {
Err(AppError::MissingMain(self.script)) Err(ScriptError::MissingMain(self.script))
} }
} }
async fn get_script_contents(&self) -> AppResult<String> { #[tracing::instrument(level = "trace", skip_all)]
let contents = fs::read_to_string(&self.script).await?; async fn get_script_contents(&self) -> Result<String, ScriptError> {
let path = CFG_PATH.join(&self.script);
Ok(contents) if path.exists() {
fs::read_to_string(path).await.map_err(ScriptError::from)
} else {
Err(ScriptError::ScriptNotFound(path))
}
} }
} }

@ -22,6 +22,7 @@ impl TaskExecutor {
} }
/// Adds all base tasks to the executor /// Adds all base tasks to the executor
#[tracing::instrument(level = "trace", skip_all)]
pub fn with_base_tasks(&mut self) -> &mut Self { pub fn with_base_tasks(&mut self) -> &mut Self {
let mut base_tasks = (*ALL_BASE_TASKS) let mut base_tasks = (*ALL_BASE_TASKS)
.iter() .iter()
@ -34,6 +35,7 @@ impl TaskExecutor {
} }
/// Adds all custom tasks to the executor /// Adds all custom tasks to the executor
#[tracing::instrument(level = "trace", skip_all)]
pub fn with_custom_tasks(&mut self) -> &mut Self { pub fn with_custom_tasks(&mut self) -> &mut Self {
let mut custom_tasks = self let mut custom_tasks = self
.distro_config .distro_config
@ -54,6 +56,7 @@ impl TaskExecutor {
} }
/// Executes all tasks /// Executes all tasks
#[tracing::instrument(level = "trace", skip_all)]
pub async fn execute(&mut self) -> AppResult<()> { pub async fn execute(&mut self) -> AppResult<()> {
for task in &self.tasks { for task in &self.tasks {
if let Some(up_task) = task.up(&self.os_config)? { if let Some(up_task) = task.up(&self.os_config)? {

Loading…
Cancel
Save