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
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.
84 lines
2.9 KiB
Go
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
|
|
}
|