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) {
data, err := s.logStore.Read(req.Msg.GetRef())
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 connect.NewResponse(&types.BytesValue{Value: data}), nil

View File

@@ -12,6 +12,7 @@ import (
"github.com/garethgeorge/backrest/pkg/restic"
"github.com/gitploy-io/cronexpr"
"go.uber.org/zap"
"google.golang.org/protobuf/proto"
)
// BackupTask is a scheduled backup operation.
@@ -162,7 +163,7 @@ func backupHelper(ctx context.Context, t Task, orchestrator *Orchestrator, plan
// schedule followup tasks
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(NewOneoffIndexSnapshotsTask(orchestrator, plan.Repo, at), TaskPriorityIndexSnapshots)

View File

@@ -11,6 +11,7 @@ import (
"github.com/garethgeorge/backrest/internal/oplog/indexutil"
"github.com/hashicorp/go-multierror"
"go.uber.org/zap"
"google.golang.org/protobuf/proto"
)
// 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 {
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")
}

View File

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

View File

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

View File

@@ -75,6 +75,8 @@ export const SettingsModal = () => {
showModal(null);
};
const users = config.auth?.users || [];
return (
<>
<Modal
@@ -101,17 +103,17 @@ export const SettingsModal = () => {
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
>
{config.auth?.users?.length === 0 ? (
{users.length > 0 ? null : (
<>
<strong>Initial backrest setup! </strong>
<p>
Backrest has detected that you do not have any users configured, please add at least one user to secure the web interface.
</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>
</>
) : null}
)}
<Form.Item label="Users" required={true}>
<Form.List
name={["auth", "users"]}
@@ -135,7 +137,7 @@ export const SettingsModal = () => {
<Col span={11}>
<Form.Item
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" />
</Form.Item>
@@ -143,7 +145,7 @@ export const SettingsModal = () => {
<Col span={11}>
<Form.Item
name={[field.name, "passwordBcrypt"]}
rules={[{ required: true }]}
rules={[{ required: true, message: "Password is required" }]}
>
<Input.Password placeholder="Password" onFocus={() => {
form.setFieldValue(["auth", "users", index, "needsBcrypt"], true);