Files
mesh-catalog/modules/restic/cmd/restic-backups/retention.go
T
jschoubben 39d5b25f95
mesh/merge-gate pass: builds restic → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
restic: take the retention from the machine's settings, so a disk can keep more or fewer restore points without a new build (novox/hq issue 416)
The keep-daily, keep-weekly and keep-monthly preferences default to
today's 14, 8 and 6. The night reads them from a file in its state and
refuses anything but a count from 1 to 9999: it then thins nothing and
says which setting is wrong, never handing a raw value to restic.
2026-10-11 03:04:52 +02:00

84 lines
2.9 KiB
Go

package main
// The retention as a preference (novox/hq issue 416, ADR 0262). The mesh writes the settings
// keep-daily, keep-weekly and keep-monthly into RetentionFile in this module's state, each defaulting
// to today's rotation, so a machine that sets none thins exactly as before. The controller does not
// type a setting yet (ADR 0164 is proposed), so this program is where a bad value is caught: it is
// refused here and never handed to restic (issue 346: a raw setting is an injection point).
import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)
// Retention is how many restore points each period keeps.
type Retention struct{ Daily, Weekly, Monthly int }
// TodaysRetention is the rotation before it was a preference (novox/hq ADR 0214), and still the
// manifest's default.
var TodaysRetention = Retention{14, 8, 6}
// RetentionFile is the file the mesh writes the retention into, in this module's state.
const RetentionFile = "retention.conf"
// A count from 1 to 9999 with no leading zero: 0 would make restic ignore the period, and a number
// past four digits is a typo, not a policy.
var count = regexp.MustCompile(`^[1-9][0-9]{0,3}$`)
// ParseRetention reads daily=, weekly= and monthly=, each once and each a count; anything else is
// refused naming the setting it came from.
func ParseRetention(text string) (Retention, error) {
settingOf := map[string]string{"daily": "keep-daily", "weekly": "keep-weekly", "monthly": "keep-monthly"}
seen := map[string]int{}
for _, l := range strings.Split(text, "\n") {
l = strings.TrimSpace(l)
if l == "" || strings.HasPrefix(l, "#") {
continue
}
k, v, ok := strings.Cut(l, "=")
if !ok {
return Retention{}, fmt.Errorf("%q is not key=value", l)
}
k, v = strings.TrimSpace(k), strings.TrimSpace(v)
setting, known := settingOf[k]
switch {
case !known:
return Retention{}, fmt.Errorf("%q is no retention this module reads (daily, weekly, monthly)", k)
case seen[k] != 0:
return Retention{}, fmt.Errorf("%s is given twice", k)
case !count.MatchString(v):
return Retention{}, fmt.Errorf("the %s setting is %q; it is a count from 1 to 9999", setting, v)
}
seen[k], _ = strconv.Atoi(v)
}
for _, k := range []string{"daily", "weekly", "monthly"} {
if seen[k] == 0 {
return Retention{}, fmt.Errorf("the %s setting is missing", settingOf[k])
}
}
return Retention{seen["daily"], seen["weekly"], seen["monthly"]}, nil
}
// retention is this machine's: today's where the module was placed by a definition older than the
// file, else what the file says.
func (b *Backups) retention() (Retention, error) {
path := filepath.Join(b.Where.State, RetentionFile)
raw, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return TodaysRetention, nil
}
if err != nil {
return Retention{}, err
}
r, err := ParseRetention(string(raw))
if err != nil {
return Retention{}, fmt.Errorf("%s: %w", path, err)
}
return r, nil
}