WP-30/31/32: backup targets, strategies and execution
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / ui (push) Has been cancelled

SMB (smbclient) and FTP/FTPS (curl with netrc) targets with encrypted
credentials and connection test; strategies with cron schedule, retention,
optional openssl AES-256 encryption; sources: PVC hostpath tar, pg_dumpall
in the Postgres pod, namespace manifests, host directory. Backup job
collects, encrypts, uploads, verifies size, records sha256 and applies
retention on the target; scheduler starts due strategies. Backups page
with target/strategy forms, run now and history. Restore guide in docs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-02 23:15:38 +02:00
parent 39af18b336
commit 6134a47ff2
31 changed files with 2718 additions and 87 deletions

View File

@ -13,6 +13,20 @@ pub struct Output {
pub trait CommandRunner: Send + Sync {
async fn run(&self, program: &str, args: &[&str]) -> Result<Output, DomainError>;
async fn read_file(&self, path: &str) -> Result<Option<String>, DomainError>;
/// Run with extra environment variables (used to pass secrets without exposing them in argv).
async fn run_env(
&self,
program: &str,
args: &[&str],
env: &[(&str, &str)],
) -> Result<Output, DomainError>;
/// Run a command and write its stdout to `file`; stderr is captured in the result.
async fn run_to_file(
&self,
program: &str,
args: &[&str],
file: &std::path::Path,
) -> Result<Output, DomainError>;
/// Run a command and forward each output line (stdout and stderr) to `out`.
async fn run_streaming(
&self,
@ -27,10 +41,24 @@ pub struct SystemCommandRunner;
#[async_trait]
impl CommandRunner for SystemCommandRunner {
async fn run(&self, program: &str, args: &[&str]) -> Result<Output, DomainError> {
let out = tokio::process::Command::new(program)
.args(args)
self.run_env(program, args, &[]).await
}
async fn run_env(
&self,
program: &str,
args: &[&str],
env: &[(&str, &str)],
) -> Result<Output, DomainError> {
let mut cmd = tokio::process::Command::new(program);
cmd.args(args)
.env("DEBIAN_FRONTEND", "noninteractive")
.env("LC_ALL", "C")
.stdin(std::process::Stdio::null());
for (k, v) in env {
cmd.env(k, v);
}
let out = cmd
.output()
.await
.map_err(|e| DomainError::Unavailable(format!("{program}: {e}")))?;
@ -41,6 +69,30 @@ impl CommandRunner for SystemCommandRunner {
})
}
async fn run_to_file(
&self,
program: &str,
args: &[&str],
file: &std::path::Path,
) -> Result<Output, DomainError> {
let f = std::fs::File::create(file)
.map_err(|e| DomainError::Storage(format!("{}: {e}", file.display())))?;
let out = tokio::process::Command::new(program)
.args(args)
.env("LC_ALL", "C")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::from(f))
.stderr(std::process::Stdio::piped())
.output()
.await
.map_err(|e| DomainError::Unavailable(format!("{program}: {e}")))?;
Ok(Output {
stdout: String::new(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
success: out.status.success(),
})
}
async fn run_streaming(
&self,
program: &str,

View File

@ -237,6 +237,22 @@ Conf openssl (3.0.16-1~deb12u1 Debian-Security:12/stable-security [amd64])\n";
) -> Result<bool, DomainError> {
unreachable!()
}
async fn run_env(
&self,
p: &str,
a: &[&str],
_: &[(&str, &str)],
) -> Result<super::super::Output, DomainError> {
self.run(p, a).await
}
async fn run_to_file(
&self,
_: &str,
_: &[&str],
_: &std::path::Path,
) -> Result<super::super::Output, DomainError> {
unreachable!()
}
async fn read_file(&self, path: &str) -> Result<Option<String>, DomainError> {
Ok(match path {
"/etc/os-release" => Some(

View File

@ -79,6 +79,22 @@ mod tests {
..Default::default()
})
}
async fn run_env(
&self,
p: &str,
a: &[&str],
_: &[(&str, &str)],
) -> Result<crate::host::Output, DomainError> {
self.run(p, a).await
}
async fn run_to_file(
&self,
_: &str,
_: &[&str],
_: &std::path::Path,
) -> Result<crate::host::Output, DomainError> {
unreachable!()
}
async fn read_file(&self, _: &str) -> Result<Option<String>, DomainError> {
Ok(None)
}