Skip to content

Commit

Permalink
fix: hook bug fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
garethgeorge committed Jun 13, 2024
1 parent 72cf45b commit 2c7d85b
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 50 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ jobs:
with:
fetch-depth: 0

# download dist artifacts from previous job
- name: Download Artifacts
uses: actions/download-artifact@v3
with:
name: release-artifacts
path: dist

- name: Get Release Artifact URL # used to append installer executables to the release after GoReleaser runs
id: geturl
run: |
Expand Down
1 change: 1 addition & 0 deletions docs/content/0.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Backrest is a web-accessible backup solution built on top of [restic](https://re
```bash [MacOS]
brew tap garethgeorge/homebrew-backrest-tap
brew install backrest
brew services start backrest
```
```bash [Arch Linux]
paru -Sy backrest
Expand Down
63 changes: 42 additions & 21 deletions docs/content/3.cookbooks/1.command-hook-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,48 @@ When run on `CONDITION_SNAPSHOT_START` command hooks have the ability to send co
- `ON_ERROR_FATAL` - If the script exits with a non-zero status, it is treated as a backup failure and error notifications are triggered.
- `ON_ERROR_IGNORE` - If the script exits with a non-zero status, the backup operation will continue and the error will be ignored.

## Examples


#### Notify a healthcheck service

Ping a healthcheck service (e.g. https://healthchecks.io/ in the example) to notify it of backup status (or failure) using a command hook.

Note that this hook example takes advantage of the fact that the hook is a golang template to render different commands based on whether an error occurred.

**Condition** `CONDITION_SNAPSHOT_END`

**Script**

```bash
#!/bin/bash
{{ if .Error -}}
curl -fsS --retry 3 https://hc-ping.com/your-uuid/fail
{{ else -}}
curl -fsS --retry 3 https://hc-ping.com/your-uuid
{{ end -}}
```
**Error Behavior:** `ON_ERROR_IGNORE`
#### (MacOS) Show system notification
Show a system notification using the `osascript` command on MacOS.
**Condition** `CONDITION_SNAPSHOT_END`, `CONDITION_PRUNE_ERROR`, `CONDITION_CHECK_ERROR`
**Script**
```bash
#!/bin/bash
{{ if .Error -}}
osascript -e 'display notification "{{ .ShellEscape .Task }} failed" with title "Backrest"'
{{ else -}}
osascript -e 'display notification "{{ .ShellEscape .Task }} succeeded" with title "Backrest"'
{{ end -}}
```
#### Check for internet connectivity
Add a hook to check for internet connectivity before running a backup.
Expand Down Expand Up @@ -74,24 +116,3 @@ fi
```
**Error Behavior:** `ON_ERROR_CANCEL`

#### Notify a healthcheck service

Ping a healthcheck service (e.g. https://healthchecks.io/ in the example) to notify it of backup status (or failure) using a command hook.

Note that this hook example takes advantage of the fact that the hook is a golang template to render different commands based on whether an error occurred.

**Condition** `CONDITION_SNAPSHOT_END`

**Script**

```bash
#!/bin/bash
{{ if .Error -}}
curl -fsS --retry 3 https://hc-ping.com/your-uuid/fail
{{ else -}}
curl -fsS --retry 3 https://hc-ping.com/your-uuid
{{ end -}}
```
**Error Behavior:** `ON_ERROR_IGNORE`
40 changes: 27 additions & 13 deletions internal/hook/hookvars.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ func (v HookVars) EventName(cond v1.Hook_Condition) string {
return "error"
case v1.Hook_CONDITION_SNAPSHOT_ERROR:
return "snapshot error"
case v1.Hook_CONDITION_SNAPSHOT_WARNING:
return "snapshot warning"
case v1.Hook_CONDITION_CHECK_START:
return "check start"
case v1.Hook_CONDITION_CHECK_ERROR:
return "check error"
case v1.Hook_CONDITION_CHECK_SUCCESS:
return "check success"
case v1.Hook_CONDITION_PRUNE_START:
return "prune start"
case v1.Hook_CONDITION_PRUNE_ERROR:
return "prune error"
case v1.Hook_CONDITION_PRUNE_SUCCESS:
return "prune success"
default:
return "unknown"
}
Expand Down Expand Up @@ -96,13 +110,9 @@ func (v HookVars) Summary() (string, error) {
switch v.Event {
case v1.Hook_CONDITION_SNAPSHOT_START:
return v.renderTemplate(templateForSnapshotStart)
case v1.Hook_CONDITION_SNAPSHOT_END:
case v1.Hook_CONDITION_SNAPSHOT_END, v1.Hook_CONDITION_SNAPSHOT_WARNING, v1.Hook_CONDITION_SNAPSHOT_SUCCESS:
return v.renderTemplate(templateForSnapshotEnd)
case v1.Hook_CONDITION_ANY_ERROR:
return v.renderTemplate(templateForError)
case v1.Hook_CONDITION_SNAPSHOT_ERROR:
return v.renderTemplate(templateForError)
case v1.Hook_CONDITION_SNAPSHOT_WARNING:
case v1.Hook_CONDITION_ANY_ERROR, v1.Hook_CONDITION_SNAPSHOT_ERROR, v1.Hook_CONDITION_CHECK_ERROR, v1.Hook_CONDITION_PRUNE_ERROR:
return v.renderTemplate(templateForError)
default:
return "unknown event", nil
Expand All @@ -123,15 +133,21 @@ func (v HookVars) renderTemplate(templ string) (string, error) {
return buf.String(), nil
}

var templateForSnapshotEnd = `
Backrest Notification for Snapshot End
var defaultTemplate = `
{{ if .Error -}}
Backrest Notification for Error
Task: "{{ .Task }}" at {{ .FormatTime .CurTime }}
Event: {{ .EventName .Event }}
Repo: {{ .Repo.Id }}
Plan: {{ .Plan.Id }}
Repo: {{ .Repo.Id }}
`

var templateForSnapshotEnd = `
Backrest Snapshot Notification
Task: {{ .Task }} at {{ .FormatTime .CurTime }}
Event: {{ .EventName .Event }}
Snapshot: {{ .SnapshotId }}
{{ if .Error -}}
Failed to create snapshot: {{ .Error }}
Error: {{ .Error }}
{{ else -}}
{{ if .SnapshotStats -}}
Expand All @@ -154,8 +170,6 @@ Backup Statistics:
{{ end }}`

var templateForError = `
Backrest Notification for Error
Task: "{{ .Task }}" at {{ .FormatTime .CurTime }}
{{ if .Error -}}
Error: {{ .Error }}
{{ end }}
Expand Down
13 changes: 9 additions & 4 deletions internal/orchestrator/tasks/taskbackup.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,16 @@ func (t *BackupTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunne
}
op.Status = v1.OperationStatus_STATUS_WARNING
op.DisplayMessage = "Partial backup, some files may not have been read completely."
}

runner.ExecuteHooks([]v1.Hook_Condition{
v1.Hook_CONDITION_SNAPSHOT_END,
}, vars)
runner.ExecuteHooks([]v1.Hook_Condition{
v1.Hook_CONDITION_SNAPSHOT_END,
}, vars)
} else {
runner.ExecuteHooks([]v1.Hook_Condition{
v1.Hook_CONDITION_SNAPSHOT_SUCCESS,
v1.Hook_CONDITION_SNAPSHOT_END,
}, vars)
}

op.SnapshotId = summary.SnapshotId
backupOp.OperationBackup.LastStatus = protoutil.BackupProgressEntryToProto(summary)
Expand Down
2 changes: 1 addition & 1 deletion internal/orchestrator/tasks/taskcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type CheckTask struct {
func NewCheckTask(repoID, planID string, force bool) Task {
return &CheckTask{
BaseTask: BaseTask{
TaskName: fmt.Sprintf("prune repo %q", repoID),
TaskName: fmt.Sprintf("check for repo %q", repoID),
TaskRepoID: repoID,
TaskPlanID: planID,
},
Expand Down
64 changes: 53 additions & 11 deletions webui/src/components/HooksFormList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,17 @@ export const HooksFormList = () => {
style={{ marginBottom: "5px" }}
>
<Form.Item name={[field.name, "conditions"]}>
<Select
mode="multiple"
allowClear
style={{ width: "100%" }}
placeholder="Runs when..."
options={proto3
.getEnumType(Hook_Condition)
.values.map((v) => ({ label: v.name, value: v.name }))}
/>
<HookConditionsTooltip>
<Select
mode="multiple"
allowClear
style={{ width: "100%" }}
placeholder="Runs when..."
options={proto3
.getEnumType(Hook_Condition)
.values.map((v) => ({ label: v.name, value: v.name }))}
/>
</HookConditionsTooltip>
</Form.Item>
<Form.Item
shouldUpdate={(prevValues, curValues) => {
Expand Down Expand Up @@ -410,8 +412,8 @@ const ItemOnErrorSelector = ({ field }: { field: FormListFieldData }) => {
<Tooltip
title={
<>
What happens when the hook fails (currently only has effect on
backup start hooks)
What happens when the hook fails (only effective on start hooks e.g.
backup start, prune start, check start)
<ul>
<li>
IGNORE - the failure is ignored, subsequent hooks and the backup
Expand Down Expand Up @@ -449,3 +451,43 @@ const requiredField = (message: string, extra?: Rule) => ({
required: true,
message: message,
});

const HookConditionsTooltip = ({ children }: { children: React.ReactNode }) => {
return (
<Tooltip
title={
<div>
Available conditions
<ul>
<li>CONDITION_ANY_ERROR - error executing any task</li>
<li>CONDITION_SNAPSHOT_START - start of a backup operation</li>
<li>
CONDITION_SNAPSHOT_END - end of backup operation (success or
failure)
</li>
<li>
CONDITION_SNAPSHOT_SUCCESS - end of successful backup operation
</li>
<li>CONDITION_SNAPSHOT_ERROR - end of failed backup</li>
<li>CONDITION_SNAPSHOT_WARNING - end of partial backup</li>
<li>CONDITION_PRUNE_START - start of prune operation</li>
<li>CONDITION_PRUNE_SUCCESS - end of successful prune</li>
<li>CONDITION_PRUNE_ERROR - end of failed prune</li>
<li>CONDITION_CHECK_START - start of check operation</li>
<li>CONDITION_CHECK_SUCCESS - end of successful check</li>
<li>CONDITION_CHECK_ERROR - end of failed check</li>
</ul>
for more info see the{" "}
<a
href="https://garethgeorge.github.io/backrest/docs/hooks"
target="_blank"
>
documentation
</a>
</div>
}
>
{children}
</Tooltip>
);
};

0 comments on commit 2c7d85b

Please sign in to comment.