agent+server: P2R-11 pre/post hook execution for backup jobs
Agent: new runner.BackupHooks struct + runHook helper invoked via /bin/sh -c (cmd.exe /C on Windows). pre_hook non-zero exit aborts the backup; post_hook always runs with RM_JOB_STATUS=succeeded|failed in env. Output streamed as 'hook(<phase>): …' log.stream lines. Hooks only run for kind=backup (other kinds skip both phases). Server: resolveBackupHooks resolves group → host default → empty, decrypts via crypto.AEAD with per-slot ad bytes, plumbs plaintext into CommandRunPayload for both schedule.fire and per-group Run-now dispatch sites. Decrypt failures degrade silently to no hook so a malformed blob can't poison every backup.
This commit is contained in:
@@ -116,15 +116,34 @@ func (r *Runner) sendFinished(jobID string, finishedAt time.Time, err error, sta
|
||||
_ = r.tx.Send(finEnv)
|
||||
}
|
||||
|
||||
// BackupHooks bundles the optional pre/post shell snippets that fire
|
||||
// around a backup. Empty fields skip that phase. Resolved server-side
|
||||
// (group → host default) before dispatch; the agent just executes
|
||||
// whatever arrives in the payload.
|
||||
type BackupHooks struct {
|
||||
Pre string
|
||||
Post string
|
||||
}
|
||||
|
||||
// RunBackup executes a backup job and reports back via the sender.
|
||||
// Returns nil on a clean (or "incomplete-but-snapshot-created") finish.
|
||||
func (r *Runner) RunBackup(ctx context.Context, jobID string, paths, excludes, tags []string) error {
|
||||
func (r *Runner) RunBackup(ctx context.Context, jobID string, paths, excludes, tags []string, hooks BackupHooks) error {
|
||||
startedAt := time.Now().UTC()
|
||||
r.sendStarted(jobID, api.JobBackup, startedAt)
|
||||
|
||||
env := r.resticEnv()
|
||||
|
||||
var seq atomic.Int64
|
||||
|
||||
// pre_hook: non-zero exit aborts the backup. The job is recorded
|
||||
// as failed with the hook's error and restic never runs.
|
||||
if hooks.Pre != "" {
|
||||
if err := r.runHook(ctx, jobID, "pre", hooks.Pre, "", &seq); err != nil {
|
||||
finishedAt := time.Now().UTC()
|
||||
r.sendFinished(jobID, finishedAt, err, nil)
|
||||
return fmt.Errorf("pre_hook failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
env := r.resticEnv()
|
||||
lastProgress := time.Now()
|
||||
|
||||
handle := func(stream string, line string, ev any) {
|
||||
@@ -173,6 +192,20 @@ func (r *Runner) RunBackup(ctx context.Context, jobID string, paths, excludes, t
|
||||
if summary != nil {
|
||||
statsBlob, _ = json.Marshal(summary)
|
||||
}
|
||||
|
||||
// post_hook: always runs regardless of backup outcome. Receives
|
||||
// RM_JOB_STATUS=succeeded|failed in env. Non-zero exit is logged
|
||||
// but does not change the recorded job status.
|
||||
if hooks.Post != "" {
|
||||
status := "succeeded"
|
||||
if err != nil {
|
||||
status = "failed"
|
||||
}
|
||||
if perr := r.runHook(ctx, jobID, "post", hooks.Post, status, &seq); perr != nil {
|
||||
slog.Warn("runner: post_hook exited non-zero", "job_id", jobID, "err", perr)
|
||||
}
|
||||
}
|
||||
|
||||
r.sendFinished(jobID, finishedAt, err, statsBlob)
|
||||
|
||||
// On a successful backup, refresh the server's snapshot projection.
|
||||
|
||||
Reference in New Issue
Block a user