From 1d73a051997167feb1abc28672aa9d9760f027e0 Mon Sep 17 00:00:00 2001 From: Rick Elrod Date: Wed, 29 Oct 2025 14:03:42 +0100 Subject: [PATCH 1/2] Basic .csmrc parsing We rely here on `serde_yaml_ng`, which is a fork of `serde_yaml` that seems at least somewhat actively maintained and used. The benefit here is that we get easy deserialization into Rust types (which we wouldn't get with something like `yaml_rust2` which just gives an AST). The disadvantage is that we're depending on a library that I wish had more community backing like the original `serde_yaml` did. The alternative here would probably be foregoing YAML entirely and using TOML instead, but it seems the rest of the Robotmk ecosystem uses YAML, so it is probably best to remain consistent here. --- Cargo.lock | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ README.md | 13 ++++++++++++ src/csmrc.rs | 38 +++++++++++++++++++++++++++++++++++ src/env.rs | 5 ++++- src/main.rs | 18 +++++++++++++---- src/robot.rs | 5 ++++- 7 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 README.md create mode 100644 src/csmrc.rs diff --git a/Cargo.lock b/Cargo.lock index f64e37e..aaced43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,8 @@ dependencies = [ "clap", "env_logger", "log", + "serde", + "serde_yaml_ng", ] [[package]] @@ -146,6 +148,12 @@ dependencies = [ "log", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -156,18 +164,40 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "jiff" version = "0.2.15" @@ -297,6 +327,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "serde" version = "1.0.228" @@ -304,6 +340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -326,6 +363,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_yaml_ng" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "strsim" version = "0.11.1" @@ -359,6 +409,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 7715434..3b86cd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,5 @@ edition = "2024" clap = { version = "4.5.50", features = ["derive", "wrap_help"] } env_logger = "0.11.8" log = "0.4.28" +serde = { version = "1.0.228", features = ["derive"] } +serde_yaml_ng = "0.10.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b266a2 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# csm - Checkmk synthetic monitoring + +(Under active development, not yet usable.) + +## Configuration: `~/.csmrc` + +You can optionally create a file, `~/.csmrc` (`%UserProfile%\.csmrc` on Windows) +to override certain defaults. This is a YAML file with the following keys +available: + +* `mamba_root_prefix` - A string which sets where the Mamba environment(s) will + be created on disk. By default, this is left up to `micromamba` and its + default root prefix is used. diff --git a/src/csmrc.rs b/src/csmrc.rs new file mode 100644 index 0000000..b03ac89 --- /dev/null +++ b/src/csmrc.rs @@ -0,0 +1,38 @@ +/// Module for reading a user's ~/.csmrc, if it exists. +use log::debug; +use serde::Deserialize; +use std::default::Default; +use std::io::{Error, ErrorKind}; +use std::path::PathBuf; + +#[derive(Debug, Default, Deserialize)] +pub struct Config { + /// Override the $MAMBA_ROOT_PREFIX when shelling out to micromamba. + #[serde(default)] + #[allow(dead_code)] + pub mamba_root_prefix: Option, +} + +impl Config { + /// Read the user's ~/.csmrc if it exists, merging with the Default instance for + /// Config. + pub fn from_csmrc() -> Result { + let home = match std::env::var("HOME") { + Ok(home) => home, + Err(_) => match std::env::var("USERPROFILE") { + Ok(home) => home, + Err(_) => return Ok(Config::default()), + }, + }; + + let csmrc_path = PathBuf::from(home).join(".csmrc"); + let Ok(csmrc_data) = std::fs::read_to_string(csmrc_path) else { + debug!("No .csmrc found, using defaults"); + return Ok(Config::default()); + }; + let config = + serde_yaml_ng::from_str(&csmrc_data).map_err(|e| Error::new(ErrorKind::InvalidData, e)); + debug!("config: {:?}", config); + config + } +} diff --git a/src/env.rs b/src/env.rs index aad9b99..42454ba 100644 --- a/src/env.rs +++ b/src/env.rs @@ -1,3 +1,5 @@ +use crate::csmrc::Config; + #[derive(Debug, clap::Subcommand)] pub enum Subcommand { /// Create an environment @@ -27,6 +29,7 @@ pub struct CreateArgs { name: Option, } -pub fn run(subcommand: Subcommand) { +pub fn run(config: Config, subcommand: Subcommand) { + println!("{:?}", config); println!("{:?}", subcommand); } diff --git a/src/main.rs b/src/main.rs index f2b104d..2ad669b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ +mod csmrc; mod env; mod robot; use clap::{Parser, Subcommand}; -use log::{LevelFilter, debug}; +use log::{LevelFilter, debug, error}; use std::fs::File; use std::io::Write; use std::path::PathBuf; @@ -29,7 +30,7 @@ enum Command { Robot(robot::Subcommand), } -fn main() { +fn main() -> Result<(), std::io::Error> { let cli = Cli::parse(); // Set up logging @@ -41,13 +42,22 @@ fn main() { let mut env_logger_builder = env_logger::Builder::new(); env_logger_builder.filter_level(default_verbosity); env_logger_builder.parse_default_env(); + env_logger_builder.format_timestamp(None); env_logger_builder.init(); let _ = create_mambarc(); + let config = match csmrc::Config::from_csmrc() { + Ok(config) => config, + Err(err) => { + error!("Failed to parse .csmrc: {}", err); + panic!("Failed to parse .csmrc as valid YAML"); + } + }; match cli.command { - Command::Env(sub) => env::run(sub), - Command::Robot(sub) => robot::run(sub), + Command::Env(sub) => env::run(config, sub), + Command::Robot(sub) => robot::run(config, sub), } + Ok(()) } fn create_mambarc() -> std::io::Result<()> { diff --git a/src/robot.rs b/src/robot.rs index 6dbfc3b..e3c81af 100644 --- a/src/robot.rs +++ b/src/robot.rs @@ -1,3 +1,5 @@ +use crate::csmrc::Config; + #[derive(Debug, clap::Subcommand)] pub enum Subcommand { /// Create a Robotmk robot @@ -13,6 +15,7 @@ pub struct CreateArgs { path: String, } -pub fn run(subcommand: Subcommand) { +pub fn run(config: Config, subcommand: Subcommand) { + println!("{:?}", config); println!("{:?}", subcommand); } From 34832f3878d1484a0194f901f0e1de77a68ff7a4 Mon Sep 17 00:00:00 2001 From: Rick Elrod Date: Wed, 29 Oct 2025 15:24:28 +0100 Subject: [PATCH 2/2] Cleanup: Pull out homedir-finding logic --- src/csmrc.rs | 15 +++++++-------- src/main.rs | 7 ++++--- src/util.rs | 5 +++++ 3 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 src/util.rs diff --git a/src/csmrc.rs b/src/csmrc.rs index b03ac89..09e2dbd 100644 --- a/src/csmrc.rs +++ b/src/csmrc.rs @@ -1,4 +1,6 @@ /// Module for reading a user's ~/.csmrc, if it exists. +use crate::util; + use log::debug; use serde::Deserialize; use std::default::Default; @@ -15,16 +17,13 @@ pub struct Config { impl Config { /// Read the user's ~/.csmrc if it exists, merging with the Default instance for - /// Config. + /// Config. Return Err if a config file was found but failed to parse, otherwise + /// Ok with the result of merging the config file values with the Default (and + /// simply the Default if no config file exists). pub fn from_csmrc() -> Result { - let home = match std::env::var("HOME") { - Ok(home) => home, - Err(_) => match std::env::var("USERPROFILE") { - Ok(home) => home, - Err(_) => return Ok(Config::default()), - }, + let Ok(home) = util::homedir() else { + return Ok(Self::default()); }; - let csmrc_path = PathBuf::from(home).join(".csmrc"); let Ok(csmrc_data) = std::fs::read_to_string(csmrc_path) else { debug!("No .csmrc found, using defaults"); diff --git a/src/main.rs b/src/main.rs index 2ad669b..905a60c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod csmrc; mod env; mod robot; +mod util; use clap::{Parser, Subcommand}; use log::{LevelFilter, debug, error}; @@ -60,6 +61,8 @@ fn main() -> Result<(), std::io::Error> { Ok(()) } +/// Create a ~/.mambarc (%UserProfile%\.mambarc on Windows) if it does not +/// exist. fn create_mambarc() -> std::io::Result<()> { let mambarc = r#" # Show the active environment in the shell prompt @@ -74,9 +77,7 @@ changeps1: True # ssl_verify: mycorpcert.crt # ssl_no_revoke: true "#; - let home = std::env::var("HOME") - .or_else(|_| std::env::var("USERPROFILE")) - .expect("Cannot determine home directory"); + let home = util::homedir().expect("Cannot determine home directory"); let mambarc_path = PathBuf::from(home).join(".mambarc"); match File::create_new(&mambarc_path) { Ok(mut file) => file.write_all(mambarc.trim_start().as_bytes())?, diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..305ee12 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,5 @@ +use std::env; + +pub fn homedir() -> Result { + env::var("HOME").or_else(|_| env::var("USERPROFILE")) +}