Fix source-group API, backup validation, and orphaned jobs #57
@@ -6,11 +6,12 @@ Three ways to trigger one:
|
||||
|
||||
1. **Scheduled** — the agent's local cron fires at the time set
|
||||
on the schedule.
|
||||
2. **Run-now** — operator clicks **Run now** on the host detail
|
||||
right rail. Posts to `/hosts/{id}/run-backup` (defaults to all
|
||||
source groups) or to a per-group form for finer control.
|
||||
3. **API** — `POST /api/hosts/{id}/jobs` with the appropriate
|
||||
payload. Same audit + dispatch path.
|
||||
2. **Run-now** — operator clicks **Run now** for a specific source
|
||||
group. This uses `POST /hosts/{id}/source-groups/{gid}/run`.
|
||||
3. **API** — use `POST /api/hosts/{id}/source-groups/{gid}/run` for a
|
||||
configured source group. The lower-level `POST /api/hosts/{id}/jobs`
|
||||
backup form requires explicit paths in `args`; an empty backup is
|
||||
rejected instead of dispatching a job that restic cannot run.
|
||||
|
||||
In every case the server creates a `jobs` row, broadcasts a
|
||||
`command.run` to the host, and lands the operator on the live
|
||||
|
||||
@@ -69,6 +69,10 @@ func (s *Server) dispatchJob(ctx context.Context, user *store.User,
|
||||
Kind: kind,
|
||||
Args: args,
|
||||
}
|
||||
if kind == api.JobBackup && len(args) == 0 {
|
||||
return res, stdhttp.StatusUnprocessableEntity, "backup_paths_required",
|
||||
"backup requires paths in args; configured backups must use POST /api/hosts/{id}/source-groups/{gid}/run"
|
||||
}
|
||||
if kind == api.JobForget {
|
||||
if !validForgetArgs(args) {
|
||||
return res, stdhttp.StatusBadRequest, "invalid_args",
|
||||
|
||||
@@ -14,6 +14,34 @@ import (
|
||||
"gitea.dcglab.co.uk/steve/restic-manager/internal/store"
|
||||
)
|
||||
|
||||
func TestRunNowBackupRejectsEmptyPaths(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ts, st := rawTestServer(t)
|
||||
cookie := loginAsAdmin(t, st)
|
||||
hostID := makeHost(t, st, "empty-backup-host")
|
||||
req, _ := stdhttp.NewRequest(stdhttp.MethodPost, ts.URL+"/api/hosts/"+hostID+"/jobs",
|
||||
bytes.NewReader([]byte(`{"kind":"backup"}`)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.AddCookie(cookie)
|
||||
res, err := stdhttp.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("post run-now: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != stdhttp.StatusUnprocessableEntity {
|
||||
t.Fatalf("status: got %d, want 422", res.StatusCode)
|
||||
}
|
||||
body := readJSONError(t, res.Body)
|
||||
if body.Code != "backup_paths_required" {
|
||||
t.Fatalf("code: got %q", body.Code)
|
||||
}
|
||||
var jobs int
|
||||
_ = st.DB().QueryRow(`SELECT COUNT(*) FROM jobs WHERE host_id = ?`, hostID).Scan(&jobs)
|
||||
if jobs != 0 {
|
||||
t.Fatalf("invalid backup created %d jobs", jobs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunNowForgetShipsRetentionGroupsAndDryRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv, ts, st := rawTestServer(t)
|
||||
|
||||
Reference in New Issue
Block a user