feat: Add environment variables to the hooks (#1518)

closes #1516

---------

Co-authored-by: Romain de Laage <romain.delaage@rdelaage.ovh>
Co-authored-by: Alexander Weiss <alex@weissfam.de>
This commit is contained in:
Romain de Laage 2025-08-29 17:43:41 +02:00 committed by GitHub
parent 33ed143f33
commit a937ae6469
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 101 additions and 27 deletions

View File

@ -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<P: ProgressBars, S: IndexedIds>(
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)

View File

@ -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<String, String>,
}
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<String, String>) -> Self {
let mut hooks = self.clone();
hooks.env = HashMap::<String, String>::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<String, String>,
) -> 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.

View File

@ -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<P> = Repository<P, IndexedStatus<FullIndex, OpenStatu
pub type CliIndexedRepo = RusticIndexedRepo<ProgressOptions>;
impl AllRepositoryOptions {
fn repository<P>(&self, po: P) -> Result<RusticRepo<P>> {
pub fn repository<P>(&self, po: P) -> Result<RusticRepo<P>> {
let backends = self.be.to_backends()?;
let repo = Repository::new_with_progress(&self.repo, &backends, po)?;
Ok(RusticRepo(repo))
}
pub fn run<T>(&self, f: impl FnOnce(CliRepo) -> Result<T>) -> Result<T> {
let hooks = self.hooks.with_context("repository");
let po = RUSTIC_APP.config().global.progress_options;
pub fn run_with_progress<P: Clone + ProgressBars, T>(
&self,
po: P,
f: impl FnOnce(RusticRepo<P>) -> Result<T>,
) -> Result<T> {
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<T>(&self, f: impl FnOnce(CliOpenRepo) -> Result<T>) -> Result<T> {
let hooks = self.hooks.with_context("repository");
pub fn run<T>(&self, f: impl FnOnce(CliRepo) -> Result<T>) -> Result<T> {
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<T>(&self, f: impl FnOnce(CliOpenRepo) -> Result<T>) -> Result<T> {
self.run(|repo| f(repo.open()?))
}
pub fn run_open_or_init_with<T: Clone>(
@ -75,13 +88,7 @@ impl AllRepositoryOptions {
init: impl FnOnce(CliRepo) -> Result<CliOpenRepo>,
f: impl FnOnce(CliOpenRepo) -> Result<T>,
) -> Result<T> {
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<P: Clone + ProgressBars, T>(
@ -89,13 +96,11 @@ impl AllRepositoryOptions {
po: P,
f: impl FnOnce(RusticIndexedRepo<P>) -> Result<T>,
) -> Result<T> {
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<T>(&self, f: impl FnOnce(CliIndexedRepo) -> Result<T>) -> Result<T> {
let po = RUSTIC_APP.config().global.progress_options;
self.run_indexed_with_progress(po, f)
self.run(|repo| f(repo.indexed()?))
}
}

View File

@ -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: [],

View File

@ -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: [],