From a937ae64694481d625f0ab06c8677bcb3e9c9ee8 Mon Sep 17 00:00:00 2001 From: Romain de Laage Date: Fri, 29 Aug 2025 17:43:41 +0200 Subject: [PATCH] feat: Add environment variables to the hooks (#1518) closes #1516 --------- Co-authored-by: Romain de Laage Co-authored-by: Alexander Weiss --- src/commands/backup.rs | 45 +++++++++++++++++-- src/config/hooks.rs | 36 ++++++++++++--- src/repository.rs | 41 +++++++++-------- ..._config__tests__default_config_passes.snap | 3 ++ ..._tests__global_env_roundtrip_passes-3.snap | 3 ++ 5 files changed, 101 insertions(+), 27 deletions(-) diff --git a/src/commands/backup.rs b/src/commands/backup.rs index 40e5a53..3e2d8fe 100644 --- a/src/commands/backup.rs +++ b/src/commands/backup.rs @@ -1,5 +1,7 @@ //! `backup` subcommand +use std::collections::HashMap; +use std::fmt::Display; use std::path::PathBuf; use std::{collections::BTreeMap, env}; @@ -18,6 +20,7 @@ use clap::ValueHint; use comfy_table::Cell; use conflate::Merge; use log::{debug, error, info, warn}; +use rustic_core::StringList; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -197,7 +200,12 @@ impl BackupCmd { } .to_indexed_ids()?; - let hooks = config.backup.hooks.with_context("backup"); + let hooks = self.hooks( + &config.backup.hooks, + "backup", + itertools::join(&config.backup.sources, ","), + ); + hooks.use_with(|| -> Result<_> { let config_snapshot_sources: Vec<_> = snapshot_opts .iter() @@ -260,6 +268,35 @@ impl BackupCmd { }) } + fn hooks(&self, hooks: &Hooks, action: &str, source: impl Display) -> Hooks { + let mut hooks_variables = + HashMap::from([("RUSTIC_ACTION".to_string(), action.to_string())]); + + if let Some(label) = &self.snap_opts.label { + let _ = hooks_variables.insert("RUSTIC_BACKUP_LABEL".to_string(), label.to_string()); + } + + let source = source.to_string(); + if !source.is_empty() { + let _ = hooks_variables.insert("RUSTIC_BACKUP_SOURCES".to_string(), source.clone()); + } + + let mut tags = StringList::default(); + tags.add_all(self.snap_opts.tags.clone()); + let tags = tags.to_string(); + if !tags.is_empty() { + let _ = hooks_variables.insert("RUSTIC_BACKUP_TAGS".to_string(), tags); + } + + let hooks = if action == "backup" { + hooks.with_context("backup") + } else { + hooks.with_context(&format!("backup {source}")) + }; + + hooks.with_env(&hooks_variables) + } + fn backup_snapshot( mut self, source: PathList, @@ -284,12 +321,14 @@ impl BackupCmd { } } - // use the correct source-specific hooks - let hooks = self.hooks.with_context(&format!("backup {source}")); + // use hooks definition before merging "backup" section + let hooks = self.hooks.clone(); // merge "backup" section from config file, if given self.merge(config.backup.clone()); + let hooks = self.hooks(&hooks, "source-specific-backup", &source); + let backup_opts = BackupOptions::default() .stdin_filename(self.stdin_filename) .stdin_command(self.stdin_command) diff --git a/src/config/hooks.rs b/src/config/hooks.rs index 7004416..aafcca0 100644 --- a/src/config/hooks.rs +++ b/src/config/hooks.rs @@ -12,6 +12,8 @@ //! - backup hooks //! - specific source-related hooks +use std::collections::HashMap; + use anyhow::Result; use conflate::Merge; use serde::{Deserialize, Serialize}; @@ -40,6 +42,10 @@ pub struct Hooks { #[serde(skip)] #[merge(skip)] pub context: String, + + #[serde(skip)] + #[merge(skip)] + pub env: HashMap, } impl Hooks { @@ -49,28 +55,46 @@ impl Hooks { hooks } - fn run_all(cmds: &[CommandInput], context: &str, what: &str) -> Result<()> { + pub fn with_env(&self, env: &HashMap) -> Self { + let mut hooks = self.clone(); + hooks.env = HashMap::::new(); + for (key, val) in env.iter() { + _ = hooks.env.insert(key.clone(), val.clone()); + } + hooks + } + + fn run_all( + cmds: &[CommandInput], + context: &str, + what: &str, + env: &HashMap, + ) -> Result<()> { + let mut env = env.clone(); + + let _ = env.insert("RUSTIC_HOOK_TYPE".to_string(), what.to_string()); + for cmd in cmds { - cmd.run(context, what, None::<(&str, &str)>)?; + cmd.run(context, what, &env)?; } Ok(()) } pub fn run_before(&self) -> Result<()> { - Self::run_all(&self.run_before, &self.context, "run-before") + Self::run_all(&self.run_before, &self.context, "run-before", &self.env) } pub fn run_after(&self) -> Result<()> { - Self::run_all(&self.run_after, &self.context, "run-after") + Self::run_all(&self.run_after, &self.context, "run-after", &self.env) } pub fn run_failed(&self) -> Result<()> { - Self::run_all(&self.run_failed, &self.context, "run-failed") + Self::run_all(&self.run_failed, &self.context, "run-failed", &self.env) } pub fn run_finally(&self) -> Result<()> { - Self::run_all(&self.run_finally, &self.context, "run-finally") + Self::run_all(&self.run_finally, &self.context, "run-finally", &self.env) } /// Run the given closure using the specified hooks. diff --git a/src/repository.rs b/src/repository.rs index 91a5f3d..1580050 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -4,6 +4,7 @@ //! application's configuration file and/or command-line options //! for specifying it. +use std::collections::HashMap; use std::fmt::Debug; use std::ops::Deref; @@ -51,22 +52,34 @@ pub type RusticIndexedRepo

= Repository; impl AllRepositoryOptions { - fn repository

(&self, po: P) -> Result> { + pub fn repository

(&self, po: P) -> Result> { let backends = self.be.to_backends()?; let repo = Repository::new_with_progress(&self.repo, &backends, po)?; Ok(RusticRepo(repo)) } - pub fn run(&self, f: impl FnOnce(CliRepo) -> Result) -> Result { - let hooks = self.hooks.with_context("repository"); - let po = RUSTIC_APP.config().global.progress_options; + pub fn run_with_progress( + &self, + po: P, + f: impl FnOnce(RusticRepo

) -> Result, + ) -> Result { + let hooks = self + .hooks + .with_env(&HashMap::from([( + "RUSTIC_ACTION".to_string(), + "repository".to_string(), + )])) + .with_context("repository"); hooks.use_with(|| f(self.repository(po)?)) } - pub fn run_open(&self, f: impl FnOnce(CliOpenRepo) -> Result) -> Result { - let hooks = self.hooks.with_context("repository"); + pub fn run(&self, f: impl FnOnce(CliRepo) -> Result) -> Result { let po = RUSTIC_APP.config().global.progress_options; - hooks.use_with(|| f(self.repository(po)?.open()?)) + self.run_with_progress(po, f) + } + + pub fn run_open(&self, f: impl FnOnce(CliOpenRepo) -> Result) -> Result { + self.run(|repo| f(repo.open()?)) } pub fn run_open_or_init_with( @@ -75,13 +88,7 @@ impl AllRepositoryOptions { init: impl FnOnce(CliRepo) -> Result, f: impl FnOnce(CliOpenRepo) -> Result, ) -> Result { - let hooks = self.hooks.with_context("repository"); - let po = RUSTIC_APP.config().global.progress_options; - hooks.use_with(|| { - f(self - .repository(po)? - .open_or_init_repository_with(do_init, init)?) - }) + self.run(|repo| f(repo.open_or_init_repository_with(do_init, init)?)) } pub fn run_indexed_with_progress( @@ -89,13 +96,11 @@ impl AllRepositoryOptions { po: P, f: impl FnOnce(RusticIndexedRepo

) -> Result, ) -> Result { - let hooks = self.hooks.with_context("repository"); - hooks.use_with(|| f(self.repository(po)?.indexed()?)) + self.run_with_progress(po, |repo| f(repo.indexed()?)) } pub fn run_indexed(&self, f: impl FnOnce(CliIndexedRepo) -> Result) -> Result { - let po = RUSTIC_APP.config().global.progress_options; - self.run_indexed_with_progress(po, f) + self.run(|repo| f(repo.indexed()?)) } } diff --git a/src/snapshots/rustic_rs__config__tests__default_config_passes.snap b/src/snapshots/rustic_rs__config__tests__default_config_passes.snap index b9a54d1..0932526 100644 --- a/src/snapshots/rustic_rs__config__tests__default_config_passes.snap +++ b/src/snapshots/rustic_rs__config__tests__default_config_passes.snap @@ -19,6 +19,7 @@ RusticConfig { run_failed: [], run_finally: [], context: "", + env: {}, }, env: {}, prometheus: None, @@ -52,6 +53,7 @@ RusticConfig { run_failed: [], run_finally: [], context: "", + env: {}, }, }, snapshot_filter: SnapshotFilter { @@ -142,6 +144,7 @@ RusticConfig { run_failed: [], run_finally: [], context: "", + env: {}, }, snapshots: [], sources: [], diff --git a/src/snapshots/rustic_rs__config__tests__global_env_roundtrip_passes-3.snap b/src/snapshots/rustic_rs__config__tests__global_env_roundtrip_passes-3.snap index ecda999..052923c 100644 --- a/src/snapshots/rustic_rs__config__tests__global_env_roundtrip_passes-3.snap +++ b/src/snapshots/rustic_rs__config__tests__global_env_roundtrip_passes-3.snap @@ -19,6 +19,7 @@ RusticConfig { run_failed: [], run_finally: [], context: "", + env: {}, }, env: { "KEY0": "VALUE0", @@ -63,6 +64,7 @@ RusticConfig { run_failed: [], run_finally: [], context: "", + env: {}, }, }, snapshot_filter: SnapshotFilter { @@ -153,6 +155,7 @@ RusticConfig { run_failed: [], run_finally: [], context: "", + env: {}, }, snapshots: [], sources: [],