diff --git a/internal/config/config.go b/internal/config/config.go index eafa5cf8..158dcd23 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 } diff --git a/internal/config/jsonstore.go b/internal/config/jsonstore.go index abcbcef5..4f622c98 100644 --- a/internal/config/jsonstore.go +++ b/internal/config/jsonstore.go @@ -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) diff --git a/internal/config/migrations/001prunepolicy.go b/internal/config/migrations/001prunepolicy.go index db779479..489303e7 100644 --- a/internal/config/migrations/001prunepolicy.go +++ b/internal/config/migrations/001prunepolicy.go @@ -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, + }, } } } diff --git a/internal/config/migrations/migrations.go b/internal/config/migrations/migrations.go index cf4bc0e6..fdd0b018 100644 --- a/internal/config/migrations/migrations.go +++ b/internal/config/migrations/migrations.go @@ -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 } diff --git a/internal/config/validate.go b/internal/config/validate.go index 718d2d3d..efef8c62 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -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 -} diff --git a/webui/src/views/AddPlanModal.tsx b/webui/src/views/AddPlanModal.tsx index 5289f00c..72c70cdb 100644 --- a/webui/src/views/AddPlanModal.tsx +++ b/webui/src/views/AddPlanModal.tsx @@ -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 =

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.

; } else if (mode === 1) { elem = ( { { /> { /> { { /> { { - 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 }); } }}>