From 173a68547990ca9752fb4f8564ca31a87ff95d03 Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 19 Nov 2025 15:04:19 +0100 Subject: [PATCH 1/3] feat: Add basic user config reading mechanism --- Cargo.lock | 31 ++++++++++ Cargo.nix | 103 ++++++++++++++++++++++++++++++- Cargo.toml | 1 + rust/stackablectl/Cargo.toml | 1 + rust/stackablectl/src/cli/mod.rs | 42 ++++++++----- rust/stackablectl/src/config.rs | 58 +++++++++++++++++ rust/stackablectl/src/lib.rs | 1 + 7 files changed, 222 insertions(+), 15 deletions(-) create mode 100644 rust/stackablectl/src/config.rs diff --git a/Cargo.lock b/Cargo.lock index 6b4fc025..ec29e282 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3252,6 +3252,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3630,6 +3639,7 @@ dependencies = [ "tera", "termion", "tokio", + "toml", "tracing", "tracing-indicatif", "tracing-subscriber", @@ -3924,6 +3934,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap 2.11.4", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.7.3" @@ -3954,6 +3979,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + [[package]] name = "tonic" version = "0.12.3" diff --git a/Cargo.nix b/Cargo.nix index 141e2f14..58ccc7af 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -10777,6 +10777,27 @@ rec { ]; }; + "serde_spanned" = rec { + crateName = "serde_spanned"; + version = "1.0.3"; + edition = "2021"; + sha256 = "14j32cqcs6jjdl1c111lz6s0hr913dnmy2kpfd75k2761ym4ahz2"; + dependencies = [ + { + name = "serde_core"; + packageId = "serde_core"; + optional = true; + usesDefaultFeatures = false; + } + ]; + features = { + "alloc" = [ "serde_core?/alloc" ]; + "default" = [ "std" "serde" ]; + "serde" = [ "dep:serde_core" ]; + "std" = [ "alloc" "serde_core?/std" ]; + }; + resolvedDefaultFeatures = [ "alloc" "serde" "std" ]; + }; "serde_urlencoded" = rec { crateName = "serde_urlencoded"; version = "0.7.1"; @@ -12109,6 +12130,11 @@ rec { packageId = "tokio"; features = [ "rt-multi-thread" "macros" "fs" "process" "io-std" ]; } + { + name = "toml"; + packageId = "toml"; + features = [ "serde" ]; + } { name = "tracing"; packageId = "tracing"; @@ -13083,6 +13109,70 @@ rec { }; resolvedDefaultFeatures = [ "codec" "default" "io" "slab" "time" ]; }; + "toml" = rec { + crateName = "toml"; + version = "0.9.8"; + edition = "2021"; + sha256 = "1n569s0dgdmqjy21wf85df7kx3vb1zgin3pc2rvy4j8lnqgqpp7h"; + dependencies = [ + { + name = "indexmap"; + packageId = "indexmap 2.11.4"; + optional = true; + usesDefaultFeatures = false; + } + { + name = "serde_core"; + packageId = "serde_core"; + optional = true; + usesDefaultFeatures = false; + features = [ "alloc" ]; + } + { + name = "serde_spanned"; + packageId = "serde_spanned"; + usesDefaultFeatures = false; + features = [ "alloc" ]; + } + { + name = "toml_datetime"; + packageId = "toml_datetime"; + usesDefaultFeatures = false; + features = [ "alloc" ]; + } + { + name = "toml_parser"; + packageId = "toml_parser"; + optional = true; + usesDefaultFeatures = false; + features = [ "alloc" ]; + } + { + name = "toml_writer"; + packageId = "toml_writer"; + optional = true; + usesDefaultFeatures = false; + features = [ "alloc" ]; + } + { + name = "winnow"; + packageId = "winnow"; + optional = true; + usesDefaultFeatures = false; + } + ]; + features = { + "debug" = [ "std" "toml_parser?/debug" "dep:anstream" "dep:anstyle" ]; + "default" = [ "std" "serde" "parse" "display" ]; + "display" = [ "dep:toml_writer" ]; + "fast_hash" = [ "preserve_order" "dep:foldhash" ]; + "parse" = [ "dep:toml_parser" "dep:winnow" ]; + "preserve_order" = [ "dep:indexmap" "std" ]; + "serde" = [ "dep:serde_core" "toml_datetime/serde" "serde_spanned/serde" ]; + "std" = [ "indexmap?/std" "serde_core?/std" "toml_parser?/std" "toml_writer?/std" "toml_datetime/std" "serde_spanned/std" ]; + }; + resolvedDefaultFeatures = [ "default" "display" "parse" "serde" "std" ]; + }; "toml_datetime" = rec { crateName = "toml_datetime"; version = "0.7.3"; @@ -13102,7 +13192,7 @@ rec { "serde" = [ "dep:serde_core" ]; "std" = [ "alloc" "serde_core?/std" ]; }; - resolvedDefaultFeatures = [ "alloc" "default" "std" ]; + resolvedDefaultFeatures = [ "alloc" "default" "serde" "std" ]; }; "toml_edit" = rec { crateName = "toml_edit"; @@ -13159,6 +13249,17 @@ rec { }; resolvedDefaultFeatures = [ "alloc" "default" "std" ]; }; + "toml_writer" = rec { + crateName = "toml_writer"; + version = "1.0.4"; + edition = "2021"; + sha256 = "1wkvcdy1ymp2qfipmb74fv3xa7m7qz7ps9hndllasx1nfda2p2yz"; + features = { + "default" = [ "std" ]; + "std" = [ "alloc" ]; + }; + resolvedDefaultFeatures = [ "alloc" "std" ]; + }; "tonic" = rec { crateName = "tonic"; version = "0.12.3"; diff --git a/Cargo.toml b/Cargo.toml index 93e88231..d0afbdbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", tera = "1.20" termion = "4.0" tokio = { version = "1.38", features = ["rt-multi-thread", "macros", "fs", "process", "io-std"] } +toml = { version = "0.9.8", features = ["serde"] } tower-http = { version = "0.5", features = ["validate-request"] } tracing = "0.1" tracing-indicatif = "0.3.9" diff --git a/rust/stackablectl/Cargo.toml b/rust/stackablectl/Cargo.toml index af107a9e..51078e75 100644 --- a/rust/stackablectl/Cargo.toml +++ b/rust/stackablectl/Cargo.toml @@ -30,6 +30,7 @@ serde.workspace = true snafu.workspace = true tera.workspace = true tokio.workspace = true +toml.workspace = true tracing-subscriber.workspace = true tracing.workspace = true tracing-indicatif.workspace = true diff --git a/rust/stackablectl/src/cli/mod.rs b/rust/stackablectl/src/cli/mod.rs index bad3bdd8..fd816d05 100644 --- a/rust/stackablectl/src/cli/mod.rs +++ b/rust/stackablectl/src/cli/mod.rs @@ -1,8 +1,8 @@ -use std::env; +use std::{env, path::Path}; use clap::{Parser, Subcommand, ValueEnum}; use directories::ProjectDirs; -use snafu::{ResultExt, Snafu}; +use snafu::{OptionExt, ResultExt, Snafu}; use stackable_cockpit::{ constants::{HELM_REPO_NAME_DEV, HELM_REPO_NAME_STABLE, HELM_REPO_NAME_TEST}, helm, @@ -19,6 +19,7 @@ use tracing::{Level, instrument}; use crate::{ args::{CommonFileArgs, CommonOperatorConfigsArgs, CommonRepoArgs}, cmds::{cache, completions, debug, demo, operator, release, stack, stacklet}, + config::UserConfig, constants::{ DEMOS_REPOSITORY_DEMOS_SUBPATH, DEMOS_REPOSITORY_STACKS_SUBPATH, DEMOS_REPOSITORY_URL_BASE, ENV_KEY_DEMO_FILES, ENV_KEY_RELEASE_FILES, ENV_KEY_STACK_FILES, REMOTE_RELEASE_FILE, @@ -55,6 +56,9 @@ pub enum Error { #[snafu(display("helm error"))] Helm { source: helm::Error }, + + #[snafu(display("failed to retrieve XDG directories"))] + RetrieveXdgDirectories, } #[derive(Debug, Parser)] @@ -148,27 +152,29 @@ impl Cli { Ok(()) } - pub fn cache_settings(&self) -> Result { + fn cache_settings(&self, cache_directory: &Path) -> Result { if self.no_cache { tracing::debug!("Cache disabled"); Ok(Settings::disabled()) } else { - let project_dir = ProjectDirs::from( - USER_DIR_QUALIFIER, - USER_DIR_ORGANIZATION_NAME, - USER_DIR_APPLICATION_NAME, - ) - .ok_or(CacheSettingsError::UserDir)?; - - let cache_dir = project_dir.cache_dir(); tracing::debug!( - cache_dir = %cache_dir.to_string_lossy(), + cache_directory = %cache_directory.to_string_lossy(), "Setting cache directory" ); - Ok(Settings::disk(cache_dir)) + Ok(Settings::disk(cache_directory)) } } + #[allow(clippy::result_large_err)] + fn xdg_directories() -> Result { + ProjectDirs::from( + USER_DIR_QUALIFIER, + USER_DIR_ORGANIZATION_NAME, + USER_DIR_APPLICATION_NAME, + ) + .context(RetrieveXdgDirectoriesSnafu) + } + #[instrument(skip_all)] pub async fn run(&self) -> Result { // FIXME (Techassi): There might be a better way to handle this with @@ -181,8 +187,16 @@ impl Cli { _ => self.add_helm_repos().context(HelmSnafu)?, } + let xdg_directories = Cli::xdg_directories()?; + let user_config_path = xdg_directories.config_dir().join("config.toml"); + + let _user_config = UserConfig::from_file(user_config_path) + .unwrap() + .unwrap_or_default(); + dbg!(_user_config); + let cache = self - .cache_settings() + .cache_settings(xdg_directories.cache_dir()) .unwrap() .try_into_cache() .await diff --git a/rust/stackablectl/src/config.rs b/rust/stackablectl/src/config.rs new file mode 100644 index 00000000..d7ae5e97 --- /dev/null +++ b/rust/stackablectl/src/config.rs @@ -0,0 +1,58 @@ +use std::path::{Path, PathBuf}; + +use serde::Deserialize; +use snafu::{ResultExt, Snafu}; + +#[derive(Debug, Default, Deserialize)] +pub struct UserConfig { + pub version: VersionOptions, +} + +#[derive(Debug, Deserialize)] +pub struct VersionOptions { + pub check_enabled: bool, +} + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to read config file from {path}", path = path.display()))] + Read { + source: std::io::Error, + path: PathBuf, + }, + + #[snafu(display("failed to deserialize config file located at {path} as TOML", path = path.display()))] + Deserialize { + source: toml::de::Error, + path: PathBuf, + }, +} + +impl UserConfig { + pub fn from_file

(path: P) -> Result, Error> + where + P: AsRef, + { + let path = path.as_ref(); + + match std::fs::read_to_string(path) { + Ok(contents) => { + let config = toml::from_str(&contents).context(DeserializeSnafu { path })?; + Ok(Some(config)) + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(Error::Read { + path: path.to_path_buf(), + source: err, + }), + } + } +} + +impl Default for VersionOptions { + fn default() -> Self { + Self { + check_enabled: true, + } + } +} diff --git a/rust/stackablectl/src/lib.rs b/rust/stackablectl/src/lib.rs index f49b1be7..9fae06c0 100644 --- a/rust/stackablectl/src/lib.rs +++ b/rust/stackablectl/src/lib.rs @@ -1,6 +1,7 @@ pub mod args; pub mod cli; pub mod cmds; +pub mod config; pub mod constants; pub mod output; pub mod utils; From af74ac8b1b52140754720b5be577a5d9696e2cca Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 19 Nov 2025 15:11:59 +0100 Subject: [PATCH 2/3] refactor: Simplify config reading --- rust/stackablectl/src/cli/mod.rs | 5 ++--- rust/stackablectl/src/config.rs | 10 ++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/rust/stackablectl/src/cli/mod.rs b/rust/stackablectl/src/cli/mod.rs index fd816d05..e127d27b 100644 --- a/rust/stackablectl/src/cli/mod.rs +++ b/rust/stackablectl/src/cli/mod.rs @@ -188,11 +188,10 @@ impl Cli { } let xdg_directories = Cli::xdg_directories()?; + // TODO (@Techassi): Move this file name to a constant let user_config_path = xdg_directories.config_dir().join("config.toml"); - let _user_config = UserConfig::from_file(user_config_path) - .unwrap() - .unwrap_or_default(); + let _user_config = UserConfig::from_file_or_default(user_config_path).unwrap(); dbg!(_user_config); let cache = self diff --git a/rust/stackablectl/src/config.rs b/rust/stackablectl/src/config.rs index d7ae5e97..4bbbae07 100644 --- a/rust/stackablectl/src/config.rs +++ b/rust/stackablectl/src/config.rs @@ -29,18 +29,16 @@ pub enum Error { } impl UserConfig { - pub fn from_file

(path: P) -> Result, Error> + /// Reads [`UserConfig`] from `path` or if not found, falls back to the default config. + pub fn from_file_or_default

(path: P) -> Result where P: AsRef, { let path = path.as_ref(); match std::fs::read_to_string(path) { - Ok(contents) => { - let config = toml::from_str(&contents).context(DeserializeSnafu { path })?; - Ok(Some(config)) - } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Ok(contents) => toml::from_str(&contents).context(DeserializeSnafu { path }), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()), Err(err) => Err(Error::Read { path: path.to_path_buf(), source: err, From 2fc916dd4145ae6bd59db810852ac6db198f8943 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 20 Nov 2025 12:44:45 +0100 Subject: [PATCH 3/3] feat: Use user config value to disable version check --- rust/stackablectl/src/cli/mod.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/rust/stackablectl/src/cli/mod.rs b/rust/stackablectl/src/cli/mod.rs index 545aa469..6fd621fc 100644 --- a/rust/stackablectl/src/cli/mod.rs +++ b/rust/stackablectl/src/cli/mod.rs @@ -212,8 +212,7 @@ impl Cli { // TODO (@Techassi): Move this file name to a constant let user_config_path = xdg_directories.config_dir().join("config.toml"); - let _user_config = UserConfig::from_file_or_default(user_config_path).unwrap(); - dbg!(_user_config); + let user_config = UserConfig::from_file_or_default(user_config_path).unwrap(); let cache_settings = self .cache_settings(xdg_directories.cache_dir()) @@ -240,13 +239,12 @@ impl Cli { .await; // Only run the version check in the background if the user runs ANY other command than - // the version command. Also only report if the current version is outdated. - let check_version_in_background = !matches!(self.subcommand, Command::Version(_)); - let release_check_future = release_check::version_notice_output( - transfer_client.clone(), - check_version_in_background, - true, - ); + // the version command and the check isn't disabled via the user config. Also only report + // if the current version is outdated. + let check_version = + !matches!(self.subcommand, Command::Version(_)) && user_config.version.check_enabled; + let release_check_future = + release_check::version_notice_output(transfer_client.clone(), check_version, true); let release_check_future = tokio::time::timeout(std::time::Duration::from_secs(2), release_check_future);