Files
mesh-catalog/modules/restic/cmd/restic-backups/main.go
T
jschoubben 3a88bca21a restic: the holder in Go
Go is the default for new module code; the holder is one binary like the licence manager, serving
the seat's verbs over the SDK and running the nights beside them. Same behaviour and tests.
2026-10-05 11:58:18 +02:00

138 lines
4.5 KiB
Go

// restic-backups (novox/hq ADR 0214, to-be 43): the machine's backups. One binary, launched by the
// machine's tool runtime and speaking MCP to it over stdio through the Go SDK (ADR 0193, ADR 0198). It
// serves the node-backup seat's three verbs — what is backed up, take one now, restore beside the
// live data — and, beside them, runs the night that happens without anyone asking.
//
// stdout is the MCP channel; everything this module says, it says on stderr.
package main
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// Seat is the role this module holds.
const Seat = "node-backup"
func say(format string, args ...any) {
fmt.Fprintf(os.Stderr, "[restic] "+format+"\n", args...)
}
func main() {
where, err := whereFromEnv()
if err != nil {
say("%v", err)
os.Exit(1)
}
b := &Backups{Where: where, Run: execRunner, Now: time.Now, Say: say}
go nights(b)
if err := stdio.Serve("", tools(b)); err != nil {
say("%v", err)
os.Exit(1)
}
}
// nights runs at the hour, every module; and at start, if a night was missed — the machine was off
// or this module was not running at the hour — one soon rather than a day later. Not at once: a
// machine just started has its stores still coming up.
func nights(b *Backups) {
hour := 3
if h, err := strconv.Atoi(os.Getenv("MESH_BACKUP_HOUR")); err == nil && h >= 0 && h < 24 {
hour = h
}
if missedANight(b.Nights(), time.Now()) {
time.Sleep(10 * time.Minute)
night(b)
}
for {
time.Sleep(time.Until(nextNight(time.Now(), hour)))
night(b)
}
}
func night(b *Backups) {
ctx := context.Background()
outcome, err := b.BackUp(ctx, "")
if err != nil {
say("the night did not run: %v", err)
return
}
var failed []string
for module, n := range outcome {
if !n.OK {
failed = append(failed, module)
}
}
if len(failed) > 0 {
say("the night left %s without a backup", strings.Join(failed, ", "))
}
// Sundays, the repository's own integrity with a sample of the data read back.
if time.Now().Weekday() == time.Sunday {
if err := b.Check(ctx); err != nil {
say("the repository does NOT check out: %v", err)
} else {
say("the repository checks out")
}
}
}
// ---- the seat's verbs --------------------------------------------------------------------------
func str(description string) map[string]any {
return map[string]any{"type": "string", "description": description}
}
func arg(a map[string]any, k string) string {
v, _ := a[k].(string)
return strings.TrimSpace(v)
}
// verb is one of the seat's verbs: listed as `<seat>.<verb>`, so the runtime serves it on the seat's
// subject.
func verb(name, description string, input map[string]any, run func(a map[string]any) (any, error)) stdio.Tool {
return stdio.Tool{Name: Seat + "." + name, Description: description, Input: input, Run: run}
}
func tools(b *Backups) []stdio.Tool {
return []stdio.Tool{
verb("backed-up", "What this machine backs up: each module, what it declared, its last good night, how many restore points are kept.",
map[string]any{"module": str("one module (optional)")},
func(a map[string]any) (any, error) { return b.BackedUp(context.Background(), arg(a, "module")) }),
verb("now", "Take a backup now, of one module or of every module on this machine — before a migration, a retirement or anything else that could go wrong. Answers when it has started; `backed-up` says how it went.",
map[string]any{"module": str("one module (optional)")},
func(a map[string]any) (any, error) {
only := arg(a, "module")
// A night of a large store outlasts any call; it is started, and its outcome recorded.
go func() {
if _, err := b.BackUp(context.Background(), only); err != nil {
say("a backup asked for now failed: %v", err)
}
}()
started := only
if started == "" {
started = "every module on this machine"
}
return map[string]any{"started": started, "follow": Seat + ".backed-up"}, nil
}),
verb("restore", "Restore one module's data from a restore point BESIDE the live data, never over it: each directory as <path>.restored-<date>. Swapping it in is a person's act.",
map[string]any{
"module": str("the module"),
"snapshot": str("the restore point (the newest when omitted)"),
"path": str("one of the module's directories (all of them when omitted)"),
},
func(a map[string]any) (any, error) {
module := arg(a, "module")
if module == "" {
return nil, fmt.Errorf("module is required")
}
return b.Restore(context.Background(), module, arg(a, "snapshot"), arg(a, "path"))
}),
}
}