diff --git a/Cargo.lock b/Cargo.lock index a654816b..223650d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3258,6 +3258,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" @@ -3637,6 +3646,7 @@ dependencies = [ "tera", "termion", "tokio", + "toml", "tracing", "tracing-indicatif", "tracing-subscriber", @@ -3931,6 +3941,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" @@ -3961,6 +3986,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 1c1c4808..07bd9f00 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -10793,6 +10793,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"; @@ -12125,6 +12146,11 @@ rec { packageId = "tokio"; features = [ "rt-multi-thread" "macros" "fs" "process" "io-std" ]; } + { + name = "toml"; + packageId = "toml"; + features = [ "serde" ]; + } { name = "tracing"; packageId = "tracing"; @@ -13105,6 +13131,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"; @@ -13124,7 +13214,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"; @@ -13181,6 +13271,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 fba4c5cf..e36ec2f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,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 30a168b5..49be1910 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 e4404633..6fd621fc 100644 --- a/rust/stackablectl/src/cli/mod.rs +++ b/rust/stackablectl/src/cli/mod.rs @@ -1,8 +1,8 @@ -use std::{env, sync::Arc}; +use std::{env, path::Path, sync::Arc}; 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, @@ -20,6 +20,7 @@ use tracing_indicatif::indicatif_eprintln; use crate::{ args::{CommonFileArgs, CommonOperatorConfigsArgs, CommonRepoArgs}, cmds::{cache, completions, debug, demo, operator, release, stack, stacklet, version}, + 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, @@ -69,6 +70,9 @@ pub enum Error { #[snafu(display("failed to initialize transfer client"))] InitializeTransferClient { source: xfer::Error }, + + #[snafu(display("failed to retrieve XDG directories"))] + RetrieveXdgDirectories, } #[derive(Debug, Snafu)] @@ -169,27 +173,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 @@ -202,7 +208,15 @@ impl Cli { _ => self.add_helm_repos().context(AddHelmReposSnafu)?, } - let cache_settings = self.cache_settings().context(RetrieveCacheSettingsSnafu)?; + 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_or_default(user_config_path).unwrap(); + + let cache_settings = self + .cache_settings(xdg_directories.cache_dir()) + .context(RetrieveCacheSettingsSnafu)?; let transfer_client = xfer::Client::new(cache_settings) .await .context(InitializeTransferClientSnafu)?; @@ -225,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); diff --git a/rust/stackablectl/src/config.rs b/rust/stackablectl/src/config.rs new file mode 100644 index 00000000..4bbbae07 --- /dev/null +++ b/rust/stackablectl/src/config.rs @@ -0,0 +1,56 @@ +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 { + /// 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) => 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, + }), + } + } +} + +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 0eb18d76..4d994091 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 release_check;