Skip to content

Commit

Permalink
feat: use disambiguated retention policy format
Browse files Browse the repository at this point in the history
  • Loading branch information
garethgeorge committed Mar 19, 2024
1 parent 3e76beb commit 5a5a229
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 73 deletions.
24 changes: 24 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()
Expand All @@ -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
}
Expand Down
13 changes: 2 additions & 11 deletions internal/config/jsonstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,16 @@ 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
}

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)
Expand Down
32 changes: 18 additions & 14 deletions internal/config/migrations/001prunepolicy.go
Original file line number Diff line number Diff line change
@@ -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,
},
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions internal/config/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
28 changes: 2 additions & 26 deletions internal/config/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package config
import (
"errors"
"fmt"
"regexp"
"strings"

v1 "github.com/garethgeorge/backrest/gen/go/v1"
Expand Down Expand Up @@ -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
}
42 changes: 22 additions & 20 deletions webui/src/views/AddPlanModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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={[
Expand All @@ -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}
Expand All @@ -524,7 +527,7 @@ const RetentionPolicyView = () => {
/>
</Form.Item>
<Form.Item
name={["retention", "keepMonthly"]}
name={["retention", "policyTimeBucketed", "monthly"]}
initialValue={3}
validateTrigger={["onChange", "onBlur"]}
required={false}
Expand All @@ -535,7 +538,7 @@ const RetentionPolicyView = () => {
/>
</Form.Item>
<Form.Item
name={["retention", "keepWeekly"]}
name={["retention", "policyTimeBucketed", "weekly"]}
initialValue={4}
validateTrigger={["onChange", "onBlur"]}
required={false}
Expand All @@ -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}
Expand All @@ -559,7 +562,7 @@ const RetentionPolicyView = () => {
/>
</Form.Item>
<Form.Item
name={["retention", "keepHourly"]}
name={["retention", "policyTimeBucketed", "hourly"]}
initialValue={24}
validateTrigger={["onChange", "onBlur"]}
required={false}
Expand All @@ -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}>
Expand Down

0 comments on commit 5a5a229

Please sign in to comment.