diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 64b0eb8..bb075fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: | diff --git a/docs/content/0.index.md b/docs/content/0.index.md index 9da032b..3063639 100644 --- a/docs/content/0.index.md +++ b/docs/content/0.index.md @@ -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 diff --git a/docs/content/3.cookbooks/1.command-hook-examples.md b/docs/content/3.cookbooks/1.command-hook-examples.md index d41adb0..3587f79 100644 --- a/docs/content/3.cookbooks/1.command-hook-examples.md +++ b/docs/content/3.cookbooks/1.command-hook-examples.md @@ -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. @@ -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` diff --git a/internal/hook/hookvars.go b/internal/hook/hookvars.go index 08b2be9..251d35e 100644 --- a/internal/hook/hookvars.go +++ b/internal/hook/hookvars.go @@ -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" } @@ -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 @@ -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 -}} @@ -154,8 +170,6 @@ Backup Statistics: {{ end }}` var templateForError = ` -Backrest Notification for Error -Task: "{{ .Task }}" at {{ .FormatTime .CurTime }} {{ if .Error -}} Error: {{ .Error }} {{ end }} diff --git a/internal/orchestrator/tasks/taskbackup.go b/internal/orchestrator/tasks/taskbackup.go index 45fc0c8..6f14c5e 100644 --- a/internal/orchestrator/tasks/taskbackup.go +++ b/internal/orchestrator/tasks/taskbackup.go @@ -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) diff --git a/internal/orchestrator/tasks/taskcheck.go b/internal/orchestrator/tasks/taskcheck.go index de4dab8..45eadf6 100644 --- a/internal/orchestrator/tasks/taskcheck.go +++ b/internal/orchestrator/tasks/taskcheck.go @@ -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, }, diff --git a/webui/src/components/HooksFormList.tsx b/webui/src/components/HooksFormList.tsx index 68d25f7..32a5b38 100644 --- a/webui/src/components/HooksFormList.tsx +++ b/webui/src/components/HooksFormList.tsx @@ -120,15 +120,17 @@ export const HooksFormList = () => { style={{ marginBottom: "5px" }} > - ({ label: v.name, value: v.name }))} + /> + { @@ -410,8 +412,8 @@ const ItemOnErrorSelector = ({ field }: { field: FormListFieldData }) => { - 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)