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 }