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:
2026-05-04 10:57:28 +01:00
parent 18b0bf976d
commit 7b1990cf11
11 changed files with 379 additions and 52 deletions
+9
View File
@@ -79,6 +79,13 @@ func (s *Server) handleRunSourceGroup(w stdhttp.ResponseWriter, r *stdhttp.Reque
return
}
// Resolve hooks (group → host default → empty). Best-effort host
// lookup; failure proceeds with no hook rather than block the run.
var preHook, postHook string
if host, herr := s.deps.Store.GetHost(r.Context(), hostID); herr == nil {
preHook, postHook = s.resolveBackupHooks(host, g)
}
// Backup invocations don't consume RetentionPolicy — that lives on
// forget. Sending the resolved set here would just be dead weight.
res, status, code, msg := s.dispatchJobWithPayload(r.Context(), user, hostID, api.JobBackup,
@@ -88,6 +95,8 @@ func (s *Server) handleRunSourceGroup(w stdhttp.ResponseWriter, r *stdhttp.Reque
Tag: g.Name,
BandwidthUpKBps: upOverride,
BandwidthDownKBps: downOverride,
PreHook: preHook,
PostHook: postHook,
})
if code != "" {
s.runGroupError(w, r, status, code, msg)