agent: RunPrune/RunCheck/RunUnlock + reportStats + admin-cred slot dispatch
Extract resticEnv/sendStarted/streamHandler/sendFinished helpers to remove boilerplate duplication across Run* methods. Add RunPrune (ships repo.stats with LastPruneAt before job.finished), RunCheck (ships stats with LastCheckStatus/LockPresent regardless of outcome), RunUnlock (ships LockPresent=false on success), and reportStats (fills size fields via RunStats when caller didn't populate them). Wire JobPrune/JobCheck/JobUnlock into the dispatcher switch; teach MsgConfigUpdate about the Slot discriminator for admin vs repo creds; add strconv import for subset-pct parsing.
This commit is contained in:
+103
-23
@@ -9,6 +9,7 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -199,32 +200,68 @@ func (d *dispatcher) handle(ctx context.Context, env api.Envelope, tx wsclient.S
|
||||
case api.MsgConfigUpdate:
|
||||
var p api.ConfigUpdatePayload
|
||||
_ = env.UnmarshalPayload(&p)
|
||||
// Merge with whatever's already in secrets.enc — empty fields
|
||||
// in the push mean "leave alone." Atomic write underneath.
|
||||
cur, err := d.secrets.Load()
|
||||
if err != nil {
|
||||
slog.Error("ws agent: load secrets for merge", "err", err)
|
||||
return nil
|
||||
slot := p.Slot
|
||||
if slot == "" {
|
||||
slot = "repo"
|
||||
}
|
||||
changed := false
|
||||
if p.RepoURL != "" && p.RepoURL != cur.URL {
|
||||
cur.URL = p.RepoURL
|
||||
changed = true
|
||||
}
|
||||
if p.RepoUsername != "" && p.RepoUsername != cur.Username {
|
||||
cur.Username = p.RepoUsername
|
||||
changed = true
|
||||
}
|
||||
if p.RepoPassword != "" && p.RepoPassword != cur.Password {
|
||||
cur.Password = p.RepoPassword
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
if err := d.secrets.Save(cur); err != nil {
|
||||
slog.Error("ws agent: persist secrets", "err", err)
|
||||
switch slot {
|
||||
case "repo":
|
||||
// Merge with whatever's already in secrets.enc — empty fields
|
||||
// in the push mean "leave alone." Atomic write underneath.
|
||||
cur, err := d.secrets.Load()
|
||||
if err != nil {
|
||||
slog.Error("ws agent: load secrets for merge", "err", err)
|
||||
return nil
|
||||
}
|
||||
slog.Info("ws agent: repo credentials updated via config.update")
|
||||
changed := false
|
||||
if p.RepoURL != "" && p.RepoURL != cur.URL {
|
||||
cur.URL = p.RepoURL
|
||||
changed = true
|
||||
}
|
||||
if p.RepoUsername != "" && p.RepoUsername != cur.Username {
|
||||
cur.Username = p.RepoUsername
|
||||
changed = true
|
||||
}
|
||||
if p.RepoPassword != "" && p.RepoPassword != cur.Password {
|
||||
cur.Password = p.RepoPassword
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
if err := d.secrets.Save(cur); err != nil {
|
||||
slog.Error("ws agent: persist secrets", "err", err)
|
||||
return nil
|
||||
}
|
||||
slog.Info("ws agent: repo credentials updated via config.update")
|
||||
}
|
||||
case "admin":
|
||||
cur, err := d.secrets.LoadAdmin()
|
||||
if err != nil && !errors.Is(err, secrets.ErrNoAdmin) {
|
||||
slog.Error("ws agent: load admin secrets", "err", err)
|
||||
return nil
|
||||
}
|
||||
// ErrNoAdmin is not an error here — we are creating the slot.
|
||||
changed := false
|
||||
if p.RepoURL != "" && p.RepoURL != cur.URL {
|
||||
cur.URL = p.RepoURL
|
||||
changed = true
|
||||
}
|
||||
if p.RepoUsername != "" && p.RepoUsername != cur.Username {
|
||||
cur.Username = p.RepoUsername
|
||||
changed = true
|
||||
}
|
||||
if p.RepoPassword != "" && p.RepoPassword != cur.Password {
|
||||
cur.Password = p.RepoPassword
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
if err := d.secrets.SaveAdmin(cur); err != nil {
|
||||
slog.Error("ws agent: persist admin secrets", "err", err)
|
||||
return nil
|
||||
}
|
||||
slog.Info("ws agent: admin credentials updated via config.update")
|
||||
}
|
||||
default:
|
||||
slog.Warn("ws agent: unknown config.update slot, ignoring", "slot", p.Slot)
|
||||
}
|
||||
|
||||
case api.MsgAgentUpdateAvail:
|
||||
@@ -318,6 +355,49 @@ func (d *dispatcher) runJob(ctx context.Context, p api.CommandRunPayload, tx wsc
|
||||
}
|
||||
slog.Info("agent: forget job complete", "job_id", p.JobID)
|
||||
}()
|
||||
case api.JobPrune:
|
||||
// Prune may require admin creds (delete authority on rest-server).
|
||||
runCreds := creds
|
||||
if p.RequiresAdminCreds {
|
||||
ac, err := d.secrets.LoadAdmin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("prune: admin creds not configured (server didn't push them): %w", err)
|
||||
}
|
||||
if ac.Empty() {
|
||||
return fmt.Errorf("prune: admin creds incomplete")
|
||||
}
|
||||
runCreds = ac
|
||||
}
|
||||
prr := runner.New(runner.Config{
|
||||
ResticBin: d.resticBin,
|
||||
RepoURL: runCreds.URL,
|
||||
RepoUsername: runCreds.Username,
|
||||
RepoPassword: runCreds.Password,
|
||||
}, tx, time.Second)
|
||||
slog.Info("agent: accepting prune job", "job_id", p.JobID, "admin_creds", p.RequiresAdminCreds)
|
||||
go func() {
|
||||
if err := prr.RunPrune(ctx, p.JobID); err != nil {
|
||||
slog.Warn("agent: prune job failed", "job_id", p.JobID, "err", err)
|
||||
}
|
||||
}()
|
||||
case api.JobCheck:
|
||||
subset := 0
|
||||
if len(p.Args) > 0 {
|
||||
subset, _ = strconv.Atoi(p.Args[0])
|
||||
}
|
||||
slog.Info("agent: accepting check job", "job_id", p.JobID, "subset_pct", subset)
|
||||
go func() {
|
||||
if err := r.RunCheck(ctx, p.JobID, subset); err != nil {
|
||||
slog.Warn("agent: check job failed", "job_id", p.JobID, "err", err)
|
||||
}
|
||||
}()
|
||||
case api.JobUnlock:
|
||||
slog.Info("agent: accepting unlock job", "job_id", p.JobID)
|
||||
go func() {
|
||||
if err := r.RunUnlock(ctx, p.JobID); err != nil {
|
||||
slog.Warn("agent: unlock job failed", "job_id", p.JobID, "err", err)
|
||||
}
|
||||
}()
|
||||
default:
|
||||
return fmt.Errorf("kind %q not implemented yet (Phase 2 lands the rest)", p.Kind)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user