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 c20375eaf5
commit 13c35b68d4
11 changed files with 379 additions and 52 deletions
+14
View File
@@ -192,6 +192,18 @@ func (s *Server) dispatchBackupForGroupCore(ctx context.Context, conn *ws.Conn,
"schedule_id", scheduleID, "group", g.Name, "err", err)
return "", err
}
// Resolve pre/post hooks (group → host default → empty) so they
// ride on the backup payload as plaintext. The host lookup is
// cheap; failure here is non-fatal (we proceed without hooks
// rather than block the backup).
var preHook, postHook string
if host, herr := s.deps.Store.GetHost(ctx, hostID); herr == nil {
preHook, postHook = s.resolveBackupHooks(host, g)
} else {
slog.Warn("schedule.fire: load host for hook resolve",
"host_id", hostID, "err", herr)
}
// Backup ignores RetentionPolicy — the forget cadence lives on
// host_repo_maintenance and is driven by the server-side ticker
// (P2R-06). Don't ship the field on backup dispatches.
@@ -201,6 +213,8 @@ func (s *Server) dispatchBackupForGroupCore(ctx context.Context, conn *ws.Conn,
Includes: g.Includes,
Excludes: g.Excludes,
Tag: g.Name,
PreHook: preHook,
PostHook: postHook,
})
if err != nil {
slog.Warn("schedule.fire: marshal command.run",