mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-08-29 04:16:39 +00:00
feat: use disambiguated retention policy format
This commit is contained in:
@@ -8,6 +8,8 @@ import (
|
||||
"sync"
|
||||
|
||||
v1 "github.com/garethgeorge/backrest/gen/go/v1"
|
||||
"github.com/garethgeorge/backrest/internal/config/migrations"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var ErrConfigNotFound = fmt.Errorf("config not found")
|
||||
@@ -49,6 +51,10 @@ type CachingValidatingStore struct {
|
||||
config *v1.Config
|
||||
}
|
||||
|
||||
func (c *CachingValidatingStore) doMigrateIfNeeded(config *v1.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CachingValidatingStore) Get() (*v1.Config, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -66,6 +72,24 @@ func (c *CachingValidatingStore) Get() (*v1.Config, error) {
|
||||
return c.config, err
|
||||
}
|
||||
|
||||
// Check if we need to migrate
|
||||
if config.Version < migrations.CurrentVersion {
|
||||
zap.S().Infof("Migrating config from version %d to %d", config.Version, migrations.CurrentVersion)
|
||||
if err := migrations.ApplyMigrations(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.Version != migrations.CurrentVersion {
|
||||
return nil, fmt.Errorf("migration failed to update config to version %d", migrations.CurrentVersion)
|
||||
}
|
||||
|
||||
// Write back the migrated config.
|
||||
if err := c.ConfigStore.Update(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the config
|
||||
if err := ValidateConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -37,10 +37,6 @@ func (f *JsonFileStore) Get() (*v1.Config, error) {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
if err := ValidateConfig(&config); err != nil {
|
||||
return nil, fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
@@ -48,14 +44,9 @@ func (f *JsonFileStore) Update(config *v1.Config) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
if err := ValidateConfig(config); err != nil {
|
||||
return fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
data, err := protojson.MarshalOptions{
|
||||
Indent: " ",
|
||||
Multiline: true,
|
||||
EmitUnpopulated: true,
|
||||
Indent: " ",
|
||||
Multiline: true,
|
||||
}.Marshal(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
|
||||
@@ -1,40 +1,44 @@
|
||||
package migrations
|
||||
|
||||
import v1 "github.com/garethgeorge/backrest/gen/go/v1"
|
||||
import (
|
||||
v1 "github.com/garethgeorge/backrest/gen/go/v1"
|
||||
)
|
||||
|
||||
func migration001PrunePolicy(config *v1.Config) {
|
||||
// loop over plans and examine prune policy's
|
||||
for _, plan := range config.Plans {
|
||||
policy := plan.GetRetention()
|
||||
if policy == nil {
|
||||
retention := plan.GetRetention()
|
||||
if retention == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if policy.Policy != nil {
|
||||
if retention.Policy != nil {
|
||||
continue // already migrated
|
||||
}
|
||||
|
||||
if policy.KeepLastN != 0 {
|
||||
if retention.KeepLastN != 0 {
|
||||
plan.Retention = &v1.RetentionPolicy{
|
||||
Policy: &v1.RetentionPolicy_PolicyKeepLastN{
|
||||
PolicyKeepLastN: policy.KeepLastN,
|
||||
PolicyKeepLastN: retention.KeepLastN,
|
||||
},
|
||||
}
|
||||
} else if policy.KeepDaily != 0 || policy.KeepHourly != 0 || policy.KeepMonthly != 0 || policy.KeepWeekly != 0 || policy.KeepYearly != 0 {
|
||||
} else if retention.KeepDaily != 0 || retention.KeepHourly != 0 || retention.KeepMonthly != 0 || retention.KeepWeekly != 0 || retention.KeepYearly != 0 {
|
||||
plan.Retention = &v1.RetentionPolicy{
|
||||
Policy: &v1.RetentionPolicy_PolicyTimeBucketed{
|
||||
PolicyTimeBucketed: &v1.RetentionPolicy_TimeBucketedCounts{
|
||||
Hourly: policy.KeepHourly,
|
||||
Daily: policy.KeepDaily,
|
||||
Weekly: policy.KeepWeekly,
|
||||
Monthly: policy.KeepMonthly,
|
||||
Yearly: policy.KeepYearly,
|
||||
Hourly: retention.KeepHourly,
|
||||
Daily: retention.KeepDaily,
|
||||
Weekly: retention.KeepWeekly,
|
||||
Monthly: retention.KeepMonthly,
|
||||
Yearly: retention.KeepYearly,
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
policy.Policy = &v1.RetentionPolicy_PolicyKeepAll{
|
||||
PolicyKeepAll: true,
|
||||
plan.Retention = &v1.RetentionPolicy{
|
||||
Policy: &v1.RetentionPolicy_PolicyKeepAll{
|
||||
PolicyKeepAll: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ package migrations
|
||||
|
||||
import v1 "github.com/garethgeorge/backrest/gen/go/v1"
|
||||
|
||||
func ApplyMigrations(config *v1.Config) {
|
||||
var CurrentVersion = int32(1)
|
||||
|
||||
func ApplyMigrations(config *v1.Config) error {
|
||||
if config.Version <= 1 {
|
||||
migration001PrunePolicy(config)
|
||||
}
|
||||
config.Version = 1
|
||||
config.Version = CurrentVersion
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package config
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
v1 "github.com/garethgeorge/backrest/gen/go/v1"
|
||||
@@ -85,32 +84,9 @@ func validatePlan(plan *v1.Plan, repos map[string]*v1.Repo) error {
|
||||
err = multierror.Append(err, fmt.Errorf("invalid cron %q: %w", plan.Cron, e))
|
||||
}
|
||||
|
||||
if plan.GetRetention() != nil {
|
||||
if e := validateRetention(plan.Retention); e != nil {
|
||||
err = multierror.Append(err, fmt.Errorf("invalid retention policy: %w", e))
|
||||
}
|
||||
if plan.Retention != nil && plan.Retention.Policy == nil {
|
||||
err = multierror.Append(err, errors.New("retention policy must be nil or must specify a policy"))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func validateRetention(policy *v1.RetentionPolicy) error {
|
||||
var err error
|
||||
if policy.KeepWithinDuration != "" {
|
||||
match, e := regexp.Match(`(\d+h)?(\d+m)?(\d+s)?`, []byte(policy.KeepWithinDuration))
|
||||
if e != nil {
|
||||
panic(e) // regex error
|
||||
}
|
||||
if !match {
|
||||
err = multierror.Append(err, fmt.Errorf("invalid keep_within_duration %q", policy.KeepWithinDuration))
|
||||
}
|
||||
if policy.KeepLastN != 0 || policy.KeepHourly != 0 || policy.KeepDaily != 0 || policy.KeepWeekly != 0 || policy.KeepMonthly != 0 || policy.KeepYearly != 0 {
|
||||
err = multierror.Append(err, fmt.Errorf("keep_within_duration cannot be used with other retention settings"))
|
||||
}
|
||||
} else {
|
||||
if policy.KeepLastN == 0 && policy.KeepHourly == 0 && policy.KeepDaily == 0 && policy.KeepWeekly == 0 && policy.KeepMonthly == 0 && policy.KeepYearly == 0 {
|
||||
err = multierror.Append(err, fmt.Errorf("at least one retention policy must be set"))
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -476,26 +476,29 @@ export const AddPlanModal = ({
|
||||
|
||||
const RetentionPolicyView = () => {
|
||||
const form = Form.useFormInstance();
|
||||
const retention = Form.useWatch('retention', { form, preserve: true }) as RetentionPolicy | undefined;
|
||||
const retention = Form.useWatch('retention', { form, preserve: true }) as any;
|
||||
|
||||
let [mode, setMode] = useState(0);
|
||||
useEffect(() => {
|
||||
if (!retention || (!retention.keepDaily && !retention.keepHourly && !retention.keepLastN && !retention.keepMonthly && !retention.keepWeekly && !retention.keepYearly)) {
|
||||
setMode(0);
|
||||
} else if (!!retention.keepLastN) {
|
||||
setMode(1);
|
||||
} else {
|
||||
setMode(2);
|
||||
const determineMode = () => {
|
||||
console.log("DERIVE MODE BASED ON RETENTION: " + JSON.stringify(retention));
|
||||
if (!retention || retention.policyTimeBucketed) {
|
||||
return 2;
|
||||
} else if (retention.policyKeepAll) {
|
||||
return 0;
|
||||
} else if (retention.policyKeepLastN) {
|
||||
return 1;
|
||||
}
|
||||
}, [retention])
|
||||
}
|
||||
|
||||
const mode = determineMode();
|
||||
|
||||
let elem: React.ReactNode = null;
|
||||
console.log("RENDERING WITH MODE: ", mode);
|
||||
if (mode === 0) {
|
||||
elem = <p>All backups are retained e.g. for append-only repos. Ensure that you manually forget / prune backups elsewhere. Backrest will register forgets performed externally on the next backup.</p>;
|
||||
} else if (mode === 1) {
|
||||
elem = (
|
||||
<Form.Item
|
||||
name={["retention", "keepLastN"]}
|
||||
name={["retention", "policyKeepLastN"]}
|
||||
initialValue={30}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
rules={[
|
||||
@@ -513,7 +516,7 @@ const RetentionPolicyView = () => {
|
||||
<Row>
|
||||
<Col span={11}>
|
||||
<Form.Item
|
||||
name={["retention", "keepYearly"]}
|
||||
name={["retention", "policyTimeBucketed", "yearly"]}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
initialValue={0}
|
||||
required={false}
|
||||
@@ -524,7 +527,7 @@ const RetentionPolicyView = () => {
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["retention", "keepMonthly"]}
|
||||
name={["retention", "policyTimeBucketed", "monthly"]}
|
||||
initialValue={3}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
required={false}
|
||||
@@ -535,7 +538,7 @@ const RetentionPolicyView = () => {
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["retention", "keepWeekly"]}
|
||||
name={["retention", "policyTimeBucketed", "weekly"]}
|
||||
initialValue={4}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
required={false}
|
||||
@@ -548,7 +551,7 @@ const RetentionPolicyView = () => {
|
||||
</Col>
|
||||
<Col span={11} offset={1}>
|
||||
<Form.Item
|
||||
name={["retention", "keepDaily"]}
|
||||
name={["retention", "policyTimeBucketed", "daily"]}
|
||||
initialValue={7}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
required={false}
|
||||
@@ -559,7 +562,7 @@ const RetentionPolicyView = () => {
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["retention", "keepHourly"]}
|
||||
name={["retention", "policyTimeBucketed", "hourly"]}
|
||||
initialValue={24}
|
||||
validateTrigger={["onChange", "onBlur"]}
|
||||
required={false}
|
||||
@@ -579,14 +582,13 @@ const RetentionPolicyView = () => {
|
||||
<Form.Item label="Retention Policy">
|
||||
<Row>
|
||||
<Radio.Group value={mode} onChange={e => {
|
||||
console.log("SELECTED: ", e.target);
|
||||
const selected = e.target.value;
|
||||
if (selected === 1) {
|
||||
form.setFieldValue("retention", { keepLastN: 30 });
|
||||
form.setFieldValue("retention", { policyKeepLastN: 30 });
|
||||
} else if (selected === 2) {
|
||||
form.setFieldValue("retention", { keepYearly: 0, keepMonthly: 3, keepWeekly: 4, keepDaily: 7, keepHourly: 24 });
|
||||
form.setFieldValue("retention", { policyTimeBucketed: { keepYearly: 0, keepMonthly: 3, keepWeekly: 4, keepDaily: 7, keepHourly: 24 } });
|
||||
} else {
|
||||
form.setFieldValue("retention", null);
|
||||
form.setFieldValue("retention", { policyKeepAll: true });
|
||||
}
|
||||
}}>
|
||||
<Radio.Button value={1}>
|
||||
|
||||
Reference in New Issue
Block a user