P2R-01: REST + WS rewire against the slim shape
Schedules CRUD now takes {cron, enabled, source_group_ids[]} with cron
parsed via robfig/cron/v3 and group membership scoped to the host.
New source-groups CRUD lives at /api/hosts/{id}/source-groups; delete
refuses with 409 if any schedule still references the group, returning
the schedule list so the UI can prompt 'remove from these schedules
first.' Repo-maintenance GET/PUT manages forget/prune/check cadences
on host_repo_maintenance — no version bump, the server-side ticker
(P2R-06) drives execution.
Per-source-group Run-now (POST /hosts/{id}/source-groups/{gid}/run)
resolves the group's includes/excludes/retention/tag and dispatches a
backup command.run with the new structured CommandRunPayload fields
(Includes/Excludes/Tag). Old per-host /hosts/{id}/run-backup and
/hosts/{id}/init-repo return 410 Gone with a redirect message.
schedule_push.go is rebuilt: buildScheduleSetPayload assembles the
slim wire shape, pushScheduleSetOnConn ships it during the on-hello
window, pushScheduleSetAsync fires after every CRUD mutation, and
dispatchScheduledJob handles agent schedule.fire by iterating the
schedule's source groups and dispatching one backup per group with
actor_kind=schedule and scheduled_id pointing at the schedule.
Auto-init at first WS connect: when the host has repo creds bound and
no init job in its history, server dispatches restic init. Restic's
'config file already exists' soft-success means re-runs against an
existing repo no-op; we don't auto-retry on failure (operator triggers
re-init manually via the danger zone in P2R-09).
api.Schedule drops Kind/Paths/Excludes/Tags/RetentionPolicy/Manual etc.
in favour of {id, cron, enabled, source_groups: [...]}. The agent
scheduler stops checking sch.Manual; cmd/agent's backup dispatch reads
Includes/Excludes/Tag instead of Args.
Tests cover the new HTTP surface end-to-end: source-groups CRUD with
in-use refusal, schedule validation (bad cron / missing groups /
foreign group), repo-maintenance auto-seed and validation, the 410
route, and buildScheduleSetPayload's wire-shape correctness. Full
suite passes; smoke env exercises auto-init dispatch on hello,
async push after schedule create, and per-source-group Run-now
landing the right paths/excludes/tag at the agent.
This commit is contained in:
+40
-22
@@ -66,13 +66,25 @@ const (
|
||||
)
|
||||
|
||||
// CommandRunPayload is the server → agent dispatch for a run-now job.
|
||||
// RetentionPolicy is populated for kind=forget jobs (raw JSON so the
|
||||
// agent doesn't need to share the typed struct definition with the
|
||||
// server's store package).
|
||||
//
|
||||
// For kind=backup, Includes/Excludes/Tag are populated from the source
|
||||
// group the operator (or schedule) targeted; the agent runs one restic
|
||||
// backup invocation per command.run, tagging the snapshot with Tag (=
|
||||
// the source group's name) so retention can target it later via
|
||||
// `restic forget --tag`.
|
||||
//
|
||||
// For kind=forget, RetentionPolicy is the typed keep-* set as raw JSON
|
||||
// (the agent doesn't share the store package's typed struct).
|
||||
//
|
||||
// Args is preserved as a generic free-form slice for kinds that don't
|
||||
// fit the structured fields (e.g. unlock takes none; init takes none).
|
||||
type CommandRunPayload struct {
|
||||
JobID string `json:"job_id"`
|
||||
Kind JobKind `json:"kind"`
|
||||
Args []string `json:"args,omitempty"`
|
||||
Includes []string `json:"includes,omitempty"`
|
||||
Excludes []string `json:"excludes,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
RetentionPolicy json.RawMessage `json:"retention_policy,omitempty"`
|
||||
}
|
||||
|
||||
@@ -171,30 +183,36 @@ type RepoStatsPayload struct {
|
||||
LockState string `json:"lock_state"` // locked|unlocked
|
||||
}
|
||||
|
||||
// Schedule is the agent-facing view of a Schedule row. (Server-side
|
||||
// CRUD shapes live in the http handlers; this is what gets pushed.)
|
||||
// Schedule is the agent-facing view of a slim Schedule row plus its
|
||||
// resolved bundle of source groups. The agent's cron only needs to know
|
||||
// when to fire (CronExpr + Enabled) and which schedule fired (ID); the
|
||||
// SourceGroups are carried for forensic logs and so a future agent that
|
||||
// elects to dispatch jobs locally has the data, but the server-side
|
||||
// dispatch path uses the schedule's group list directly. Manual
|
||||
// schedules are gone — Run-now targets a source group, not a schedule.
|
||||
type Schedule struct {
|
||||
ID string `json:"id"`
|
||||
Kind JobKind `json:"kind"`
|
||||
CronExpr string `json:"cron_expr"`
|
||||
Paths []string `json:"paths,omitempty"`
|
||||
Excludes []string `json:"excludes,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
RetentionPolicy json.RawMessage `json:"retention_policy,omitempty"`
|
||||
Options json.RawMessage `json:"options,omitempty"`
|
||||
PreHook string `json:"pre_hook,omitempty"`
|
||||
PostHook string `json:"post_hook,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
// Manual schedules are not added to the agent's local cron — they
|
||||
// fire only when the operator clicks a Run-now button. The agent
|
||||
// can ignore them entirely; we ship them in the payload only so
|
||||
// the operator can edit them on a still-disconnected agent.
|
||||
Manual bool `json:"manual,omitempty"`
|
||||
ID string `json:"id"`
|
||||
CronExpr string `json:"cron_expr"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SourceGroups []ScheduleSourceGroup `json:"source_groups,omitempty"`
|
||||
}
|
||||
|
||||
// ScheduleSourceGroup is the resolved-at-push-time view of a source
|
||||
// group attached to a schedule. The agent doesn't need source_group_id
|
||||
// — Name is the snapshot tag and is unique per host.
|
||||
type ScheduleSourceGroup struct {
|
||||
Name string `json:"name"`
|
||||
Includes []string `json:"includes,omitempty"`
|
||||
Excludes []string `json:"excludes,omitempty"`
|
||||
RetentionPolicy json.RawMessage `json:"retention_policy,omitempty"`
|
||||
RetryMax int `json:"retry_max,omitempty"`
|
||||
RetryBackoffSeconds int `json:"retry_backoff_seconds,omitempty"`
|
||||
}
|
||||
|
||||
// ScheduleSetPayload — server pushes the full canonical schedule list
|
||||
// for a host. Agent reconciles its local cron and replies with
|
||||
// ScheduleAckPayload carrying the same Version.
|
||||
// ScheduleAckPayload carrying the same Version. An empty Schedules
|
||||
// list is a valid push that disables every cron entry.
|
||||
type ScheduleSetPayload struct {
|
||||
Version int64 `json:"version"`
|
||||
Schedules []Schedule `json:"schedules"`
|
||||
|
||||
Reference in New Issue
Block a user