feat: support keep-all retention policy for append-only backups

This commit is contained in:
garethgeorge
2024-02-04 03:14:00 -08:00
parent f1084cab48
commit f163c02d7d
6 changed files with 50 additions and 22 deletions

View File

@@ -408,6 +408,12 @@ func (s *BackrestHandler) ClearHistory(ctx context.Context, req *connect.Request
func (s *BackrestHandler) GetLogs(ctx context.Context, req *connect.Request[v1.LogDataRequest]) (*connect.Response[types.BytesValue], error) { func (s *BackrestHandler) GetLogs(ctx context.Context, req *connect.Request[v1.LogDataRequest]) (*connect.Response[types.BytesValue], error) {
data, err := s.logStore.Read(req.Msg.GetRef()) data, err := s.logStore.Read(req.Msg.GetRef())
if err != nil { if err != nil {
if errors.Is(err, rotatinglog.ErrFileNotFound) {
return connect.NewResponse(&types.BytesValue{
Value: []byte(fmt.Sprintf("file associated with log %v not found, it may have rotated out of the log history", req.Msg.GetRef())),
}), nil
}
return nil, fmt.Errorf("get log data %v: %w", req.Msg.GetRef(), err) return nil, fmt.Errorf("get log data %v: %w", req.Msg.GetRef(), err)
} }
return connect.NewResponse(&types.BytesValue{Value: data}), nil return connect.NewResponse(&types.BytesValue{Value: data}), nil

View File

@@ -12,6 +12,7 @@ import (
"github.com/garethgeorge/backrest/pkg/restic" "github.com/garethgeorge/backrest/pkg/restic"
"github.com/gitploy-io/cronexpr" "github.com/gitploy-io/cronexpr"
"go.uber.org/zap" "go.uber.org/zap"
"google.golang.org/protobuf/proto"
) )
// BackupTask is a scheduled backup operation. // BackupTask is a scheduled backup operation.
@@ -162,7 +163,7 @@ func backupHelper(ctx context.Context, t Task, orchestrator *Orchestrator, plan
// schedule followup tasks // schedule followup tasks
at := time.Now() at := time.Now()
if plan.Retention != nil { if plan.Retention != nil && !proto.Equal(plan.Retention, &v1.RetentionPolicy{}) {
orchestrator.ScheduleTask(NewOneoffForgetTask(orchestrator, plan, op.SnapshotId, at), TaskPriorityForget) orchestrator.ScheduleTask(NewOneoffForgetTask(orchestrator, plan, op.SnapshotId, at), TaskPriorityForget)
} }
orchestrator.ScheduleTask(NewOneoffIndexSnapshotsTask(orchestrator, plan.Repo, at), TaskPriorityIndexSnapshots) orchestrator.ScheduleTask(NewOneoffIndexSnapshotsTask(orchestrator, plan.Repo, at), TaskPriorityIndexSnapshots)

View File

@@ -11,6 +11,7 @@ import (
"github.com/garethgeorge/backrest/internal/oplog/indexutil" "github.com/garethgeorge/backrest/internal/oplog/indexutil"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"go.uber.org/zap" "go.uber.org/zap"
"google.golang.org/protobuf/proto"
) )
// ForgetTask tracks a forget operation. // ForgetTask tracks a forget operation.
@@ -59,7 +60,7 @@ func (t *ForgetTask) Next(now time.Time) *time.Time {
} }
func (t *ForgetTask) Run(ctx context.Context) error { func (t *ForgetTask) Run(ctx context.Context) error {
if t.plan.Retention == nil { if t.plan.Retention == nil || proto.Equal(t.plan.Retention, &v1.RetentionPolicy{}) {
return errors.New("plan does not have a retention policy") return errors.New("plan does not have a retention policy")
} }

View File

@@ -12,6 +12,7 @@ import {
Card, Card,
Col, Col,
Collapse, Collapse,
FormInstance,
} from "antd"; } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useShowModal } from "../components/ModalManager"; import { useShowModal } from "../components/ModalManager";
@@ -329,7 +330,7 @@ export const AddPlanModal = ({
</Tooltip> </Tooltip>
{/* Plan.retention */} {/* Plan.retention */}
<RetentionPolicyView policy={template?.retention} /> <RetentionPolicyView policy={template?.retention} form={form} />
{/* Plan.hooks */} {/* Plan.hooks */}
@@ -364,17 +365,23 @@ export const AddPlanModal = ({
); );
}; };
const RetentionPolicyView = ({ policy }: { policy?: RetentionPolicy }) => { const RetentionPolicyView = ({ form, policy }: { policy?: RetentionPolicy, form: FormInstance }) => {
enum PolicyType { enum PolicyType {
TimeBased, TimeBased,
CountBased, CountBased,
None,
} }
policy = policy || new RetentionPolicy(); policy = policy || new RetentionPolicy();
const [policyType, setPolicyType] = useState<PolicyType>( let defaultPolicyType = PolicyType.None;
policy.keepLastN ? PolicyType.CountBased : PolicyType.TimeBased if (policy.keepLastN) {
); defaultPolicyType = PolicyType.CountBased;
} else if (policy) {
defaultPolicyType = PolicyType.TimeBased;
}
const [policyType, setPolicyType] = useState<PolicyType>(defaultPolicyType);
let elem = null; let elem = null;
switch (policyType) { switch (policyType) {
@@ -464,6 +471,8 @@ const RetentionPolicyView = ({ policy }: { policy?: RetentionPolicy }) => {
</Form.Item> </Form.Item>
); );
break; break;
case PolicyType.None:
elem = <p>All backups are retained e.g. for append-only repos.</p>
} }
return ( return (
@@ -474,6 +483,9 @@ const RetentionPolicyView = ({ policy }: { policy?: RetentionPolicy }) => {
value={policyType} value={policyType}
onChange={(e) => { onChange={(e) => {
setPolicyType(e.target.value); setPolicyType(e.target.value);
if (e.target.value === PolicyType.None) {
form.resetFields(["retention"]);
}
}} }}
> >
<Radio.Button value={PolicyType.CountBased}> <Radio.Button value={PolicyType.CountBased}>
@@ -486,6 +498,11 @@ const RetentionPolicyView = ({ policy }: { policy?: RetentionPolicy }) => {
By Time Period By Time Period
</Tooltip> </Tooltip>
</Radio.Button> </Radio.Button>
<Radio.Button value={PolicyType.None}>
<Tooltip title="All backups will be retained. Note that this may result in slow backups if the set of snapshots grows very large.">
None
</Tooltip>
</Radio.Button>
</Radio.Group> </Radio.Group>
</Row> </Row>
<br /> <br />

View File

@@ -119,10 +119,9 @@ export const App: React.FC = () => {
<small style={{ color: "rgba(255,255,255,0.3)", fontSize: "0.6em" }}> <small style={{ color: "rgba(255,255,255,0.3)", fontSize: "0.6em" }}>
{config && config.host ? "Host: " + config.host : undefined} {config && config.host ? "Host: " + config.host : undefined}
</small> </small>
</h1>
<Button <Button
type="text" type="text"
style={{ position: "absolute", right: "10px" }} style={{ marginLeft: "10px" }}
onClick={() => { onClick={() => {
setAuthToken(""); setAuthToken("");
window.location.reload(); window.location.reload();
@@ -130,6 +129,8 @@ export const App: React.FC = () => {
> >
Logout Logout
</Button> </Button>
</h1>
</Header> </Header>
<Layout> <Layout>
<Sider width={300} style={{ background: colorBgContainer }}> <Sider width={300} style={{ background: colorBgContainer }}>

View File

@@ -75,6 +75,8 @@ export const SettingsModal = () => {
showModal(null); showModal(null);
}; };
const users = config.auth?.users || [];
return ( return (
<> <>
<Modal <Modal
@@ -101,17 +103,17 @@ export const SettingsModal = () => {
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }} wrapperCol={{ span: 16 }}
> >
{config.auth?.users?.length === 0 ? ( {users.length > 0 ? null : (
<> <>
<strong>Initial backrest setup! </strong> <strong>Initial backrest setup! </strong>
<p> <p>
Backrest has detected that you do not have any users configured, please add at least one user to secure the web interface. Backrest has detected that you do not have any users configured, please add at least one user to secure the web interface.
</p> </p>
<p> <p>
You can add more users later or can reset users by editing the configuration file. You can add more users later or, if you forget your password, reset users by editing the configuration file (typically in $HOME/.backrest/config.json)
</p> </p>
</> </>
) : null} )}
<Form.Item label="Users" required={true}> <Form.Item label="Users" required={true}>
<Form.List <Form.List
name={["auth", "users"]} name={["auth", "users"]}
@@ -135,7 +137,7 @@ export const SettingsModal = () => {
<Col span={11}> <Col span={11}>
<Form.Item <Form.Item
name={[field.name, "name"]} name={[field.name, "name"]}
rules={[{ required: true }, { pattern: namePattern, message: "Name must be alphanumeric with dashes or underscores as separators" }]} rules={[{ required: true, message: "Name is required" }, { pattern: namePattern, message: "Name must be alphanumeric with dashes or underscores as separators" }]}
> >
<Input placeholder="Username" /> <Input placeholder="Username" />
</Form.Item> </Form.Item>
@@ -143,7 +145,7 @@ export const SettingsModal = () => {
<Col span={11}> <Col span={11}>
<Form.Item <Form.Item
name={[field.name, "passwordBcrypt"]} name={[field.name, "passwordBcrypt"]}
rules={[{ required: true }]} rules={[{ required: true, message: "Password is required" }]}
> >
<Input.Password placeholder="Password" onFocus={() => { <Input.Password placeholder="Password" onFocus={() => {
form.setFieldValue(["auth", "users", index, "needsBcrypt"], true); form.setFieldValue(["auth", "users", index, "needsBcrypt"], true);