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:
@@ -42,8 +42,8 @@ func TestSourceGroupHooksRoundTrip(t *testing.T) {
|
||||
|
||||
g := &SourceGroup{
|
||||
ID: ulid.Make().String(), HostID: hostID, Name: "etc",
|
||||
PreHook: []byte("ENC-PRE"),
|
||||
PostHook: []byte("ENC-POST"),
|
||||
PreHook: "ENC-PRE",
|
||||
PostHook: "ENC-POST",
|
||||
}
|
||||
if err := st.CreateSourceGroup(context.Background(), g); err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
@@ -52,16 +52,16 @@ func TestSourceGroupHooksRoundTrip(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("get: %v", err)
|
||||
}
|
||||
if string(got.PreHook) != "ENC-PRE" {
|
||||
if got.PreHook != "ENC-PRE" {
|
||||
t.Fatalf("PreHook: got %q, want ENC-PRE", got.PreHook)
|
||||
}
|
||||
if string(got.PostHook) != "ENC-POST" {
|
||||
if got.PostHook != "ENC-POST" {
|
||||
t.Fatalf("PostHook: got %q, want ENC-POST", got.PostHook)
|
||||
}
|
||||
|
||||
// Update: clear PreHook, change PostHook.
|
||||
got.PreHook = nil
|
||||
got.PostHook = []byte("ENC-POST-2")
|
||||
got.PreHook = ""
|
||||
got.PostHook = "ENC-POST-2"
|
||||
if err := st.UpdateSourceGroup(context.Background(), got); err != nil {
|
||||
t.Fatalf("update: %v", err)
|
||||
}
|
||||
@@ -69,10 +69,10 @@ func TestSourceGroupHooksRoundTrip(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("get: %v", err)
|
||||
}
|
||||
if got.PreHook != nil {
|
||||
t.Fatalf("PreHook: want nil after clear, got %q", got.PreHook)
|
||||
if got.PreHook != "" {
|
||||
t.Fatalf("PreHook: want empty after clear, got %q", got.PreHook)
|
||||
}
|
||||
if string(got.PostHook) != "ENC-POST-2" {
|
||||
if got.PostHook != "ENC-POST-2" {
|
||||
t.Fatalf("PostHook: got %q, want ENC-POST-2", got.PostHook)
|
||||
}
|
||||
}
|
||||
@@ -82,25 +82,25 @@ func TestHostHookDefaultsRoundTrip(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
hostID := makeHostInStore(t, st, "host-hooks-host")
|
||||
|
||||
if err := st.SetHostHooks(context.Background(), hostID, []byte("PRE"), []byte("POST")); err != nil {
|
||||
if err := st.SetHostHooks(context.Background(), hostID, "PRE", "POST"); err != nil {
|
||||
t.Fatalf("set: %v", err)
|
||||
}
|
||||
h, err := st.GetHost(context.Background(), hostID)
|
||||
if err != nil {
|
||||
t.Fatalf("get: %v", err)
|
||||
}
|
||||
if string(h.PreHookDefault) != "PRE" || string(h.PostHookDefault) != "POST" {
|
||||
if h.PreHookDefault != "PRE" || h.PostHookDefault != "POST" {
|
||||
t.Fatalf("after set: pre=%q post=%q", h.PreHookDefault, h.PostHookDefault)
|
||||
}
|
||||
// Clear by passing nil.
|
||||
if err := st.SetHostHooks(context.Background(), hostID, nil, nil); err != nil {
|
||||
// Clear by passing empty strings.
|
||||
if err := st.SetHostHooks(context.Background(), hostID, "", ""); err != nil {
|
||||
t.Fatalf("clear: %v", err)
|
||||
}
|
||||
h, err = st.GetHost(context.Background(), hostID)
|
||||
if err != nil {
|
||||
t.Fatalf("get: %v", err)
|
||||
}
|
||||
if h.PreHookDefault != nil || h.PostHookDefault != nil {
|
||||
t.Fatalf("after clear: pre=%v post=%v (want nil)", h.PreHookDefault, h.PostHookDefault)
|
||||
if h.PreHookDefault != "" || h.PostHookDefault != "" {
|
||||
t.Fatalf("after clear: pre=%q post=%q (want empty)", h.PreHookDefault, h.PostHookDefault)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user