Reject backup jobs without paths
CI / Test (store) (pull_request) Successful in 5s
CI / Lint (pull_request) Successful in 12s
CI / Test (rest) (pull_request) Successful in 20s
CI / Build (windows/amd64) (pull_request) Successful in 8s
CI / Build (linux/amd64) (pull_request) Successful in 8s
CI / Build (linux/arm64) (pull_request) Successful in 7s
CI / Test (server-http) (pull_request) Successful in 1m36s
e2e / Playwright vs docker-compose (pull_request) Successful in 1m26s
CI / Test (store) (pull_request) Successful in 5s
CI / Lint (pull_request) Successful in 12s
CI / Test (rest) (pull_request) Successful in 20s
CI / Build (windows/amd64) (pull_request) Successful in 8s
CI / Build (linux/amd64) (pull_request) Successful in 8s
CI / Build (linux/arm64) (pull_request) Successful in 7s
CI / Test (server-http) (pull_request) Successful in 1m36s
e2e / Playwright vs docker-compose (pull_request) Successful in 1m26s
This commit is contained in:
@@ -6,11 +6,12 @@ Three ways to trigger one:
|
|||||||
|
|
||||||
1. **Scheduled** — the agent's local cron fires at the time set
|
1. **Scheduled** — the agent's local cron fires at the time set
|
||||||
on the schedule.
|
on the schedule.
|
||||||
2. **Run-now** — operator clicks **Run now** on the host detail
|
2. **Run-now** — operator clicks **Run now** for a specific source
|
||||||
right rail. Posts to `/hosts/{id}/run-backup` (defaults to all
|
group. This uses `POST /hosts/{id}/source-groups/{gid}/run`.
|
||||||
source groups) or to a per-group form for finer control.
|
3. **API** — use `POST /api/hosts/{id}/source-groups/{gid}/run` for a
|
||||||
3. **API** — `POST /api/hosts/{id}/jobs` with the appropriate
|
configured source group. The lower-level `POST /api/hosts/{id}/jobs`
|
||||||
payload. Same audit + dispatch path.
|
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
|
In every case the server creates a `jobs` row, broadcasts a
|
||||||
`command.run` to the host, and lands the operator on the live
|
`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,
|
Kind: kind,
|
||||||
Args: args,
|
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 kind == api.JobForget {
|
||||||
if !validForgetArgs(args) {
|
if !validForgetArgs(args) {
|
||||||
return res, stdhttp.StatusBadRequest, "invalid_args",
|
return res, stdhttp.StatusBadRequest, "invalid_args",
|
||||||
|
|||||||
@@ -14,6 +14,34 @@ import (
|
|||||||
"gitea.dcglab.co.uk/steve/restic-manager/internal/store"
|
"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) {
|
func TestRunNowForgetShipsRetentionGroupsAndDryRun(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
srv, ts, st := rawTestServer(t)
|
srv, ts, st := rawTestServer(t)
|
||||||
|
|||||||
Reference in New Issue
Block a user