Compare commits
22 Commits
6db128c80a
...
feat-add-h
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a25eca929 | |||
| afc568a4e9 | |||
| 73ce57b122 | |||
| fb31691451 | |||
| 0c0305301e | |||
| cd92ce697e | |||
| 4cfb634397 | |||
| cb466747cd | |||
| 3f544409f1 | |||
| 6ff6e8759e | |||
| 07832050dc | |||
| 70fb02c0b0 | |||
| a9e0f04b8e | |||
| b05f507993 | |||
| bef0763de8 | |||
| ca32c5a041 | |||
| 8a8b62b249 | |||
| acd1f2efef | |||
| e944677876 | |||
| cd307aeafd | |||
| ad09b6c906 | |||
| 6bdf45534f |
111
.gitea/workflows/cd.yaml
Normal file
111
.gitea/workflows/cd.yaml
Normal file
@@ -0,0 +1,111 @@
|
||||
name: CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
DOCKER_REGISTRY: gitea.t000-n.de
|
||||
|
||||
jobs:
|
||||
check-changes:
|
||||
name: Check changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changes: ${{ steps.filter.outputs.code }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Get changed files
|
||||
id: filter
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
code:
|
||||
- 'src/**'
|
||||
- 'Dockerfile'
|
||||
- 'gitea/workflows/**'
|
||||
|
||||
build_and_push:
|
||||
name: Build and push
|
||||
needs:
|
||||
- check-changes
|
||||
if: ${{ needs.check-changes.outputs.changes != '0' }}
|
||||
strategy:
|
||||
matrix:
|
||||
arch:
|
||||
- amd64
|
||||
- arm64
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
- linux_${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
- id: meta
|
||||
run: |
|
||||
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}' | tr '[:upper:]' '[:lower:]') >> $GITHUB_OUTPUT
|
||||
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
push: true
|
||||
provenance: false
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/t.behrendt/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}-${{ matrix.arch }}
|
||||
|
||||
create_tag:
|
||||
name: Create tag
|
||||
needs:
|
||||
- check-changes
|
||||
if: ${{ needs.check-changes.outputs.changes != '0' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tag: ${{ steps.tag.outputs.new-tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: https://gitea.t000-n.de/t.behrendt/conventional-semantic-git-tag-increment@0.0.2
|
||||
id: tag
|
||||
with:
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
- run: |
|
||||
git tag ${{ steps.tag.outputs.new-tag }}
|
||||
git push origin ${{ steps.tag.outputs.new-tag }}
|
||||
- name: Set output
|
||||
run: |
|
||||
echo "tag=${{ steps.tag.outputs.new-tag }}" >> $GITHUB_OUTPUT
|
||||
|
||||
create_manifest:
|
||||
name: Create manifest
|
||||
needs:
|
||||
- build_and_push
|
||||
- create_tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- id: meta
|
||||
run: |
|
||||
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}' | tr '[:upper:]' '[:lower:]') >> $GITHUB_OUTPUT
|
||||
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
- run: |
|
||||
docker manifest create ${{ env.DOCKER_REGISTRY }}/t.behrendt/${{ steps.meta.outputs.REPO_NAME }}:${{ needs.create_tag.outputs.tag }} \
|
||||
${{ env.DOCKER_REGISTRY }}/t.behrendt/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/t.behrendt/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}-arm64
|
||||
|
||||
docker manifest push ${{ env.DOCKER_REGISTRY }}/t.behrendt/${{ steps.meta.outputs.REPO_NAME }}:${{ needs.create_tag.outputs.tag }}
|
||||
33
.gitea/workflows/ci.yaml
Normal file
33
.gitea/workflows/ci.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docker image
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
- linux_amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: false
|
||||
provenance: false
|
||||
tags: |
|
||||
backupsidecar:ci-test
|
||||
|
||||
check-syntax:
|
||||
name: Check syntax
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Check syntax
|
||||
run: |
|
||||
bash -n src/backup.sh
|
||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM alpine:3.22
|
||||
|
||||
RUN apk update && apk add --no-cache \
|
||||
bash \
|
||||
curl \
|
||||
restic \
|
||||
postgresql-client \
|
||||
jq
|
||||
|
||||
RUN curl -O https://dl.min.io/client/mc/release/linux-amd64/mc \
|
||||
&& chmod +x mc \
|
||||
&& mv mc /usr/local/bin/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY src/backup.sh /app/backup.sh
|
||||
|
||||
RUN chmod +x /app/backup.sh
|
||||
|
||||
ENTRYPOINT ["/app/backup.sh"]
|
||||
373
README.md
373
README.md
@@ -1,3 +1,372 @@
|
||||
# backupsidecar
|
||||
# BackupSidecar
|
||||
|
||||
Backup sidecar that automatically creates backups of one PVC and saves it to another PVC via restic
|
||||
BackupSidecar is a lightweight backup and restore solution designed to run as a cron job in Kubernetes. It automates backups and restores using Restic and supports both directory and PostgreSQL database operations. Optional notifications can be sent via Gotify to keep you informed of operation results.
|
||||
|
||||
## Configuration
|
||||
|
||||
BackupSidecar is configured through environment variables. Below is a breakdown of the available settings.
|
||||
|
||||
### General Settings
|
||||
|
||||
These variables apply to both backup and restore operations.
|
||||
|
||||
- **`OPERATION_MODE`** _(optional)_ - Defines the operation type (`backup` or `restore`). Defaults to `backup`.
|
||||
- **`BACKUP_MODE`** _(optional)_ - Defines the backup type (`directory`, `postgres`, or `s3`). Defaults to `directory`.
|
||||
- **`RESTIC_PASSWORD`** _(required)_ - The encryption password for Restic.
|
||||
- **`RESTIC_REPOSITORY`** _(required)_ - The URI of the Restic repository (e.g., `rest:http://your-rest-server:8000/backup`).
|
||||
- **`RESTIC_REST_USERNAME`** _(optional)_ - The username for REST server authentication.
|
||||
- **`RESTIC_REST_PASSWORD`** _(optional)_ - The password for REST server authentication.
|
||||
- **`ENABLE_GOTIFY`** _(optional)_ - Enable Gotify notifications. Set to `true` to enable, any other value or unset disables notifications. Defaults to `true`.
|
||||
- **`GOTIFYHOST`** _(required when ENABLE_GOTIFY=true)_ - The Gotify server URL.
|
||||
- **`GOTIFYTOKEN`** _(required when ENABLE_GOTIFY=true)_ - The API token for Gotify.
|
||||
- **`GOTIFYTOPIC`** _(required when ENABLE_GOTIFY=true)_ - The topic under which backup notifications will be sent.
|
||||
|
||||
### Directory Operations
|
||||
|
||||
When running in `directory` mode, the following variables must be set:
|
||||
|
||||
**For Backup Operations:**
|
||||
|
||||
- **`SOURCEDIR`** _(required)_ - The path of the directory to be backed up.
|
||||
|
||||
**For Restore Operations:**
|
||||
|
||||
- **`RESTOREDIR`** _(required)_ - The path where files should be restored to.
|
||||
- **`RESTORE_SNAPSHOT_ID`** _(optional)_ - The specific snapshot ID to restore (defaults to `latest`).
|
||||
|
||||
### PostgreSQL Operations
|
||||
|
||||
For `postgres` mode, the following database-related variables are required:
|
||||
|
||||
**Common Variables:**
|
||||
|
||||
- **`PGHOST`** _(required)_ - The hostname of the PostgreSQL server.
|
||||
- **`PGDATABASE`** _(required)_ - The name of the database.
|
||||
- **`PGUSER`** _(required)_ - The PostgreSQL username.
|
||||
- **`PGPORT`** _(optional)_ - The port for PostgreSQL (defaults to `5432`).
|
||||
- **`PGPASSWORD`** _(optional)_ - The password for authentication. Setting this prevents interactive prompts.
|
||||
|
||||
**Backup-Specific Variables:**
|
||||
|
||||
- **`PG_DUMP_ARGS`** _(optional)_ - Additional flags for `pg_dump`.
|
||||
|
||||
**Restore-Specific Variables:**
|
||||
|
||||
- **`RESTORE_SNAPSHOT_ID`** _(optional)_ - The specific snapshot ID to restore (defaults to `latest`).
|
||||
- **`PSQL_ARGS`** _(optional)_ - Additional flags for `psql` (e.g., `--single-transaction`).
|
||||
|
||||
### S3 Operations
|
||||
|
||||
For `s3` mode, the following S3-related variables are required:
|
||||
|
||||
**Common Variables:**
|
||||
|
||||
- **`S3_BUCKET`** _(required)_ - The name of the S3 bucket to backup/restore.
|
||||
- **`S3_ENDPOINT`** _(required)_ - The S3 endpoint URL (e.g., `http://minio:9000` for MinIO).
|
||||
- **`MINIO_ACCESS_KEY`** _(required)_ - The MinIO access key for S3 authentication.
|
||||
- **`MINIO_SECRET_KEY`** _(required)_ - The MinIO secret key for S3 authentication.
|
||||
- **`S3_PREFIX`** _(optional)_ - Optional path prefix within the bucket to backup/restore.
|
||||
|
||||
**Restore-Specific Variables:**
|
||||
|
||||
- **`RESTORE_SNAPSHOT_ID`** _(optional)_ - The specific snapshot ID to restore (defaults to `latest`).
|
||||
|
||||
## Dependencies
|
||||
|
||||
Ensure the following commands are available in the container:
|
||||
|
||||
- `restic`
|
||||
- `curl`
|
||||
- `jq`
|
||||
- `pg_dump` _(only required for PostgreSQL backup operations)_
|
||||
- `psql` _(only required for PostgreSQL restore operations)_
|
||||
- `mc` _(only required for S3 operations)_
|
||||
|
||||
## Usage
|
||||
|
||||
### Backup Operations
|
||||
|
||||
Example Kubernetes CronJob manifest for running BackupSidecar as a cron job for directory backups in minimal configuration:
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: backupsidecar-cron
|
||||
namespace: authentik
|
||||
spec:
|
||||
schedule: "0 7 * * *"
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 5
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
activeDeadlineSeconds: 300
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: backupsidecar
|
||||
image: backupsidecar:latest
|
||||
env:
|
||||
- name: RESTIC_REPOSITORY
|
||||
value: "rest:http://rest-server:8000/backup"
|
||||
- name: RESTIC_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: backupsidecar-secret
|
||||
key: restic_password
|
||||
- name: BACKUP_MODE
|
||||
value: "directory" # or "postgres"
|
||||
- name: SOURCEDIR
|
||||
value: "/data/source"
|
||||
- name: ENABLE_GOTIFY
|
||||
value: "true"
|
||||
- name: GOTIFYHOST
|
||||
value: "http://gotify.example.com"
|
||||
- name: GOTIFYTOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: backupsidecar-secret
|
||||
key: gotify_token
|
||||
- name: GOTIFYTOPIC
|
||||
value: "Backup Notification"
|
||||
# (For PostgreSQL mode, add PGHOST, PGDATABASE, PGUSER, PGPORT, PGPASSWORD)
|
||||
volumeMounts:
|
||||
- name: source-data
|
||||
mountPath: /data/source
|
||||
restartPolicy: OnFailure
|
||||
volumes:
|
||||
- name: source-data
|
||||
persistentVolumeClaim:
|
||||
claimName: source-data-pvc
|
||||
```
|
||||
|
||||
### Restore Operations
|
||||
|
||||
Example Kubernetes Job manifest for running BackupSidecar to restore a directory:
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: backupsidecar-restore
|
||||
namespace: authentik
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
activeDeadlineSeconds: 600
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: backupsidecar
|
||||
image: backupsidecar:latest
|
||||
env:
|
||||
- name: OPERATION_MODE
|
||||
value: "restore"
|
||||
- name: BACKUP_MODE
|
||||
value: "directory"
|
||||
- name: RESTOREDIR
|
||||
value: "/data/restore"
|
||||
- name: RESTORE_SNAPSHOT_ID
|
||||
value: "abc123def456" # optional, defaults to latest
|
||||
- name: RESTIC_REPOSITORY
|
||||
value: "rest:http://rest-server:8000/backup"
|
||||
- name: RESTIC_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: backupsidecar-secret
|
||||
key: restic_password
|
||||
- name: GOTIFYHOST
|
||||
value: "http://gotify.example.com"
|
||||
- name: GOTIFYTOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: backupsidecar-secret
|
||||
key: gotify_token
|
||||
- name: GOTIFYTOPIC
|
||||
value: "Restore Notification"
|
||||
volumeMounts:
|
||||
- name: restore-data
|
||||
mountPath: /data/restore
|
||||
volumes:
|
||||
- name: restore-data
|
||||
persistentVolumeClaim:
|
||||
claimName: restore-data-pvc
|
||||
```
|
||||
|
||||
Example Kubernetes Job manifest for running BackupSidecar to restore a PostgreSQL database:
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: backupsidecar-postgres-restore
|
||||
namespace: authentik
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
activeDeadlineSeconds: 600
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: backupsidecar
|
||||
image: backupsidecar:latest
|
||||
env:
|
||||
- name: OPERATION_MODE
|
||||
value: "restore"
|
||||
- name: BACKUP_MODE
|
||||
value: "postgres"
|
||||
- name: PGHOST
|
||||
value: "postgres.example.com"
|
||||
- name: PGDATABASE
|
||||
value: "mydatabase"
|
||||
- name: PGUSER
|
||||
value: "myuser"
|
||||
- name: PGPASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secret
|
||||
key: password
|
||||
- name: PGPORT
|
||||
value: "5432"
|
||||
- name: RESTORE_SNAPSHOT_ID
|
||||
value: "abc123def456" # optional, defaults to latest
|
||||
- name: PSQL_ARGS
|
||||
value: "--single-transaction" # optional
|
||||
- name: RESTIC_REPOSITORY
|
||||
value: "rest:http://rest-server:8000/backup"
|
||||
- name: RESTIC_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: backupsidecar-secret
|
||||
key: restic_password
|
||||
- name: GOTIFYHOST
|
||||
value: "http://gotify.example.com"
|
||||
- name: GOTIFYTOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: backupsidecar-secret
|
||||
key: gotify_token
|
||||
- name: GOTIFYTOPIC
|
||||
value: "Database Restore Notification"
|
||||
```
|
||||
|
||||
Example Kubernetes Job manifest for running BackupSidecar to backup an S3 bucket:
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: backupsidecar-s3-backup
|
||||
namespace: authentik
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
activeDeadlineSeconds: 600
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: backupsidecar
|
||||
image: backupsidecar:latest
|
||||
env:
|
||||
- name: OPERATION_MODE
|
||||
value: "backup"
|
||||
- name: BACKUP_MODE
|
||||
value: "s3"
|
||||
- name: S3_BUCKET
|
||||
value: "my-bucket"
|
||||
- name: S3_ENDPOINT
|
||||
value: "http://minio:9000"
|
||||
- name: S3_PREFIX
|
||||
value: "data" # optional
|
||||
- name: MINIO_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: minio-secret
|
||||
key: access_key
|
||||
- name: MINIO_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: minio-secret
|
||||
key: secret_key
|
||||
- name: RESTIC_REPOSITORY
|
||||
value: "rest:http://rest-server:8000/backup"
|
||||
- name: RESTIC_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: backupsidecar-secret
|
||||
key: restic_password
|
||||
- name: GOTIFYHOST
|
||||
value: "http://gotify.example.com"
|
||||
- name: GOTIFYTOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: backupsidecar-secret
|
||||
key: gotify_token
|
||||
- name: GOTIFYTOPIC
|
||||
value: "S3 Backup Notification"
|
||||
```
|
||||
|
||||
Example Kubernetes Job manifest for running BackupSidecar to restore an S3 bucket:
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: backupsidecar-s3-restore
|
||||
namespace: authentik
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
activeDeadlineSeconds: 600
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: backupsidecar
|
||||
image: backupsidecar:latest
|
||||
env:
|
||||
- name: OPERATION_MODE
|
||||
value: "restore"
|
||||
- name: BACKUP_MODE
|
||||
value: "s3"
|
||||
- name: S3_BUCKET
|
||||
value: "my-bucket"
|
||||
- name: S3_ENDPOINT
|
||||
value: "http://minio:9000"
|
||||
- name: S3_PREFIX
|
||||
value: "data" # optional
|
||||
- name: RESTORE_SNAPSHOT_ID
|
||||
value: "abc123def456" # optional, defaults to latest
|
||||
- name: MINIO_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: minio-secret
|
||||
key: access_key
|
||||
- name: MINIO_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: minio-secret
|
||||
key: secret_key
|
||||
- name: RESTIC_REPOSITORY
|
||||
value: "rest:http://rest-server:8000/backup"
|
||||
- name: RESTIC_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: backupsidecar-secret
|
||||
key: restic_password
|
||||
- name: GOTIFYHOST
|
||||
value: "http://gotify.example.com"
|
||||
- name: GOTIFYTOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: backupsidecar-secret
|
||||
key: gotify_token
|
||||
- name: GOTIFYTOPIC
|
||||
value: "S3 Restore Notification"
|
||||
```
|
||||
|
||||
## Notifications
|
||||
|
||||
The script can send success or failure notifications via Gotify when enabled. To enable notifications, set `ENABLE_GOTIFY=true` and provide the required Gotify configuration variables (`GOTIFYHOST`, `GOTIFYTOKEN`, `GOTIFYTOPIC`). When notifications are disabled, backup status messages are still logged to the console.
|
||||
|
||||
3
renovate.json
Normal file
3
renovate.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||
}
|
||||
437
src/backup.sh
Normal file
437
src/backup.sh
Normal file
@@ -0,0 +1,437 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
#######################################
|
||||
# Date format for logging.
|
||||
#######################################
|
||||
LOG_DATE_FORMAT="%Y-%m-%dT%T"
|
||||
|
||||
#######################################
|
||||
# Log a message with a timestamp.
|
||||
# Arguments:
|
||||
# Message to log.
|
||||
#######################################
|
||||
log() {
|
||||
echo "$(date +"$LOG_DATE_FORMAT") - $*"
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Determine operation mode from the environment only.
|
||||
# Valid values: "backup" or "restore".
|
||||
# Default to "backup" if not provided.
|
||||
#######################################
|
||||
OPERATION_MODE="${OPERATION_MODE:-backup}"
|
||||
|
||||
#######################################
|
||||
# Determine backup mode from the environment only.
|
||||
# Valid values: "directory", "postgres", or "s3".
|
||||
# Default to "directory" if not provided.
|
||||
#######################################
|
||||
BACKUP_MODE="${BACKUP_MODE:-directory}"
|
||||
|
||||
#######################################
|
||||
# Check for required external commands.
|
||||
#######################################
|
||||
REQUIRED_CMDS=(restic curl jq)
|
||||
if [ "$BACKUP_MODE" = "postgres" ]; then
|
||||
if [ "$OPERATION_MODE" = "backup" ]; then
|
||||
REQUIRED_CMDS+=(pg_dump)
|
||||
elif [ "$OPERATION_MODE" = "restore" ]; then
|
||||
REQUIRED_CMDS+=(psql)
|
||||
fi
|
||||
elif [ "$BACKUP_MODE" = "s3" ]; then
|
||||
REQUIRED_CMDS+=(mc)
|
||||
fi
|
||||
|
||||
for cmd in "${REQUIRED_CMDS[@]}"; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
log "Error: Required command '$cmd' is not installed."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
#######################################
|
||||
# Validate common required environment variables.
|
||||
#######################################
|
||||
# Gotify notification settings (optional).
|
||||
# Set ENABLE_GOTIFY to "true" to enable notifications, any other value or unset disables them.
|
||||
ENABLE_GOTIFY="${ENABLE_GOTIFY:-true}"
|
||||
|
||||
if [ "$ENABLE_GOTIFY" = "true" ]; then
|
||||
: "${GOTIFYHOST:?Environment variable GOTIFYHOST is not set (required when ENABLE_GOTIFY=true)}"
|
||||
: "${GOTIFYTOKEN:?Environment variable GOTIFYTOKEN is not set (required when ENABLE_GOTIFY=true)}"
|
||||
: "${GOTIFYTOPIC:?Environment variable GOTIFYTOPIC is not set (required when ENABLE_GOTIFY=true)}"
|
||||
else
|
||||
log "Gotify notifications disabled. Backup status will be logged to console only."
|
||||
fi
|
||||
|
||||
# Restic encryption password.
|
||||
: "${RESTIC_PASSWORD:?Environment variable RESTIC_PASSWORD is not set}"
|
||||
|
||||
# Use the repository URI directly from the environment.
|
||||
# Example: export RESTIC_REPOSITORY="rest:http://your-rest-server:8000/backup"
|
||||
: "${RESTIC_REPOSITORY:?Environment variable RESTIC_REPOSITORY is not set}"
|
||||
|
||||
#######################################
|
||||
# Validate operation mode.
|
||||
#######################################
|
||||
case "$OPERATION_MODE" in
|
||||
backup|restore)
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown operation mode '$OPERATION_MODE'. Valid modes are 'backup' and 'restore'." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
#######################################
|
||||
# Validate mode-specific environment variables.
|
||||
#######################################
|
||||
case "$BACKUP_MODE" in
|
||||
directory)
|
||||
if [ "$OPERATION_MODE" = "backup" ]; then
|
||||
: "${SOURCEDIR:?Environment variable SOURCEDIR is not set (required for directory backup mode)}"
|
||||
elif [ "$OPERATION_MODE" = "restore" ]; then
|
||||
: "${RESTOREDIR:?Environment variable RESTOREDIR is not set (required for directory restore mode)}"
|
||||
fi
|
||||
;;
|
||||
postgres)
|
||||
: "${PGHOST:?Environment variable PGHOST is not set (required for PostgreSQL mode)}"
|
||||
: "${PGDATABASE:?Environment variable PGDATABASE is not set (required for PostgreSQL mode)}"
|
||||
: "${PGUSER:?Environment variable PGUSER is not set (required for PostgreSQL mode)}"
|
||||
# Optional: default PGPORT to 5432.
|
||||
: "${PGPORT:=5432}"
|
||||
if [ -z "${PGPASSWORD:-}" ]; then
|
||||
if [ "$OPERATION_MODE" = "backup" ]; then
|
||||
echo "Warning: Environment variable PGPASSWORD is not set. pg_dump may fail if authentication is required."
|
||||
elif [ "$OPERATION_MODE" = "restore" ]; then
|
||||
echo "Warning: Environment variable PGPASSWORD is not set. psql may fail if authentication is required."
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
s3)
|
||||
: "${S3_BUCKET:?Environment variable S3_BUCKET is not set (required for S3 mode)}"
|
||||
: "${S3_ENDPOINT:?Environment variable S3_ENDPOINT is not set (required for S3 mode)}"
|
||||
: "${MINIO_ACCESS_KEY:?Environment variable MINIO_ACCESS_KEY is not set (required for S3 mode)}"
|
||||
: "${MINIO_SECRET_KEY:?Environment variable MINIO_SECRET_KEY is not set (required for S3 mode)}"
|
||||
# Optional: S3 path prefix
|
||||
: "${S3_PREFIX:=}"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown backup mode '$BACKUP_MODE'. Valid modes are 'directory', 'postgres', and 's3'." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
#######################################
|
||||
# Build the Gotify URL (only if Gotify is enabled).
|
||||
#######################################
|
||||
if [ "$ENABLE_GOTIFY" = "true" ]; then
|
||||
GOTIFYURL="${GOTIFYHOST}/message?token=${GOTIFYTOKEN}"
|
||||
fi
|
||||
|
||||
#######################################
|
||||
# Send a notification via Gotify.
|
||||
# Arguments:
|
||||
# message: The message to send.
|
||||
#######################################
|
||||
send_notification() {
|
||||
local message="$1"
|
||||
|
||||
# Only send notification if Gotify is enabled
|
||||
if [ "$ENABLE_GOTIFY" != "true" ]; then
|
||||
log "$message"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! curl -s -X POST "$GOTIFYURL" -F "title=${GOTIFYTOPIC}" -F "message=${message}" >/dev/null; then
|
||||
log "Warning: Failed to send notification with message: ${message}"
|
||||
fi
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Run the backup using restic.
|
||||
# The --no-cache flag disables local caching.
|
||||
# Arguments:
|
||||
# $1 - The source directory to back up.
|
||||
#######################################
|
||||
run_restic_backup() {
|
||||
local source_dir="$1"
|
||||
cd "${source_dir}"
|
||||
log "Starting backup of '${source_dir}' to repository ${RESTIC_REPOSITORY}"
|
||||
# Capture both stdout and stderr in a variable
|
||||
backup_output=$(restic -r "${RESTIC_REPOSITORY}" backup --no-cache --json --verbose . 2>&1)
|
||||
# Optionally, also print the output to the console:
|
||||
echo "$backup_output"
|
||||
# Parse the JSON lines output for the summary message
|
||||
summary=$(echo "$backup_output" | jq -r 'select(.message_type=="summary") | "Snapshot " + (.snapshot_id // "none") + ": " + "files new: " + (.files_new|tostring) + ", files changed: " + (.files_changed|tostring) + ", data added: " + (.data_added|tostring) + " bytes in " + (.total_duration|tostring) + " sec"')
|
||||
# Check exit code of restic backup (assuming restic exits non-zero on error)
|
||||
if [ $? -eq 0 ]; then
|
||||
msg="Backup successful. $summary"
|
||||
log "$msg"
|
||||
send_notification "$msg"
|
||||
else
|
||||
exit_code=$?
|
||||
msg="Backup failed with error code ${exit_code}. $backup_output"
|
||||
log "$msg"
|
||||
send_notification "$msg"
|
||||
exit "$exit_code"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
#######################################
|
||||
# Backup a directory (regular mode).
|
||||
#######################################
|
||||
backup_directory() {
|
||||
run_restic_backup "${SOURCEDIR}"
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Backup a PostgreSQL database.
|
||||
# Dumps the database to a temporary directory and then backs it up.
|
||||
#######################################
|
||||
backup_postgres() {
|
||||
log "Starting PostgreSQL backup for database '${PGDATABASE}' on host '${PGHOST}'"
|
||||
|
||||
# Create a temporary directory for the database dump.
|
||||
TEMP_BACKUP_DIR=$(mktemp -d)
|
||||
log "Created temporary directory: ${TEMP_BACKUP_DIR}"
|
||||
|
||||
local dump_file="${TEMP_BACKUP_DIR}/dump.sql"
|
||||
log "Dumping PostgreSQL database to ${dump_file}..."
|
||||
if pg_dump -h "${PGHOST}" -p "${PGPORT}" -U "${PGUSER}" ${PG_DUMP_ARGS:-} "${PGDATABASE}" > "${dump_file}"; then
|
||||
log "Database dump created successfully."
|
||||
else
|
||||
local exit_code=$?
|
||||
local msg="PostgreSQL dump failed with error code ${exit_code}"
|
||||
log "$msg"
|
||||
send_notification "$msg"
|
||||
exit "$exit_code"
|
||||
fi
|
||||
|
||||
# Back up the directory containing the dump.
|
||||
run_restic_backup "${TEMP_BACKUP_DIR}"
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Run the restore using restic.
|
||||
# Arguments:
|
||||
# $1 - The target directory to restore to.
|
||||
# $2 - Optional snapshot ID to restore (defaults to latest).
|
||||
#######################################
|
||||
run_restic_restore() {
|
||||
local target_dir="$1"
|
||||
local snapshot_id="$2"
|
||||
|
||||
log "Starting restore from repository ${RESTIC_REPOSITORY} to '${target_dir}'"
|
||||
log "Using snapshot: ${snapshot_id}"
|
||||
|
||||
# Create target directory if it doesn't exist
|
||||
mkdir -p "${target_dir}"
|
||||
|
||||
# Capture both stdout and stderr in a variable
|
||||
restore_output=$(restic -r "${RESTIC_REPOSITORY}" restore "${snapshot_id}" --target "${target_dir}" --no-cache --json --verbose 2>&1)
|
||||
# Optionally, also print the output to the console:
|
||||
echo "$restore_output"
|
||||
|
||||
# Parse the JSON lines output for the summary message
|
||||
summary=$(echo "$restore_output" | jq -r 'select(.message_type=="summary") | "Restore completed: " + (.files_restored|tostring) + " files restored, " + (.bytes_restored|tostring) + " bytes in " + (.total_duration|tostring) + " sec"' 2>/dev/null || echo "Restore completed")
|
||||
|
||||
# Check exit code of restic restore
|
||||
if [ $? -eq 0 ]; then
|
||||
msg="Restore successful. $summary"
|
||||
log "$msg"
|
||||
send_notification "$msg"
|
||||
else
|
||||
exit_code=$?
|
||||
msg="Restore failed with error code ${exit_code}. $restore_output"
|
||||
log "$msg"
|
||||
send_notification "$msg"
|
||||
exit "$exit_code"
|
||||
fi
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Restore a directory (regular mode).
|
||||
#######################################
|
||||
restore_directory() {
|
||||
local snapshot_id="${RESTORE_SNAPSHOT_ID:-latest}"
|
||||
run_restic_restore "${RESTOREDIR}" "${snapshot_id}"
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Restore a PostgreSQL database.
|
||||
# Restores the database dump from the backup and applies it to the database.
|
||||
#######################################
|
||||
restore_postgres() {
|
||||
local snapshot_id="${RESTORE_SNAPSHOT_ID:-latest}"
|
||||
log "Starting PostgreSQL restore for database '${PGDATABASE}' on host '${PGHOST}'"
|
||||
|
||||
# Create a temporary directory for the restore.
|
||||
TEMP_RESTORE_DIR=$(mktemp -d)
|
||||
log "Created temporary directory: ${TEMP_RESTORE_DIR}"
|
||||
|
||||
# Restore the backup to the temporary directory
|
||||
run_restic_restore "${TEMP_RESTORE_DIR}" "${snapshot_id}"
|
||||
|
||||
local dump_file="${TEMP_RESTORE_DIR}/dump.sql"
|
||||
if [ ! -f "${dump_file}" ]; then
|
||||
local msg="PostgreSQL restore failed. Database dump file not found at ${dump_file}"
|
||||
log "$msg"
|
||||
send_notification "$msg"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Restoring PostgreSQL database from ${dump_file}..."
|
||||
if psql -h "${PGHOST}" -p "${PGPORT}" -U "${PGUSER}" -d "${PGDATABASE}" ${PSQL_ARGS:-} < "${dump_file}"; then
|
||||
local msg="PostgreSQL database restored successfully"
|
||||
log "$msg"
|
||||
send_notification "$msg"
|
||||
else
|
||||
local exit_code=$?
|
||||
local msg="PostgreSQL restore failed with error code ${exit_code}"
|
||||
log "$msg"
|
||||
send_notification "$msg"
|
||||
exit "$exit_code"
|
||||
fi
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Backup an S3 bucket.
|
||||
# Syncs the S3 bucket to a temporary directory and then backs it up.
|
||||
#######################################
|
||||
backup_s3() {
|
||||
log "Starting S3 backup for bucket '${S3_BUCKET}' at endpoint '${S3_ENDPOINT}'"
|
||||
|
||||
# Create a temporary directory for the S3 sync.
|
||||
TEMP_BACKUP_DIR=$(mktemp -d)
|
||||
log "Created temporary directory: ${TEMP_BACKUP_DIR}"
|
||||
|
||||
# Configure MinIO Client alias
|
||||
local alias_name="backupsidecar"
|
||||
if ! mc alias set "${alias_name}" "${S3_ENDPOINT}" "${MINIO_ACCESS_KEY}" "${MINIO_SECRET_KEY}"; then
|
||||
local msg="Failed to configure MinIO client alias"
|
||||
log "$msg"
|
||||
send_notification "$msg"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build S3 path
|
||||
local s3_path="${alias_name}/${S3_BUCKET}"
|
||||
if [ -n "${S3_PREFIX}" ]; then
|
||||
s3_path="${s3_path}/${S3_PREFIX}"
|
||||
fi
|
||||
|
||||
log "Syncing S3 bucket from ${s3_path} to ${TEMP_BACKUP_DIR}..."
|
||||
if mc mirror "${s3_path}" "${TEMP_BACKUP_DIR}" --remove; then
|
||||
log "S3 sync completed successfully."
|
||||
else
|
||||
local exit_code=$?
|
||||
local msg="S3 sync failed with error code ${exit_code}"
|
||||
log "$msg"
|
||||
send_notification "$msg"
|
||||
exit "$exit_code"
|
||||
fi
|
||||
|
||||
# Back up the directory containing the S3 content.
|
||||
run_restic_backup "${TEMP_BACKUP_DIR}"
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Restore an S3 bucket.
|
||||
# Restores the S3 content from the backup and syncs it back to S3.
|
||||
#######################################
|
||||
restore_s3() {
|
||||
local snapshot_id="${RESTORE_SNAPSHOT_ID:-latest}"
|
||||
log "Starting S3 restore for bucket '${S3_BUCKET}' at endpoint '${S3_ENDPOINT}'"
|
||||
|
||||
# Create a temporary directory for the restore.
|
||||
TEMP_RESTORE_DIR=$(mktemp -d)
|
||||
log "Created temporary directory: ${TEMP_RESTORE_DIR}"
|
||||
|
||||
# Restore the backup to the temporary directory
|
||||
run_restic_restore "${TEMP_RESTORE_DIR}" "${snapshot_id}"
|
||||
|
||||
# Configure MinIO Client alias
|
||||
local alias_name="backupsidecar"
|
||||
if ! mc alias set "${alias_name}" "${S3_ENDPOINT}" "${MINIO_ACCESS_KEY}" "${MINIO_SECRET_KEY}"; then
|
||||
local msg="Failed to configure MinIO client alias"
|
||||
log "$msg"
|
||||
send_notification "$msg"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build S3 path
|
||||
local s3_path="${alias_name}/${S3_BUCKET}"
|
||||
if [ -n "${S3_PREFIX}" ]; then
|
||||
s3_path="${s3_path}/${S3_PREFIX}"
|
||||
fi
|
||||
|
||||
log "Syncing restored content from ${TEMP_RESTORE_DIR} to ${s3_path}..."
|
||||
if mc mirror "${TEMP_RESTORE_DIR}" "${s3_path}" --remove; then
|
||||
local msg="S3 restore completed successfully"
|
||||
log "$msg"
|
||||
send_notification "$msg"
|
||||
else
|
||||
local exit_code=$?
|
||||
local msg="S3 restore failed with error code ${exit_code}"
|
||||
log "$msg"
|
||||
send_notification "$msg"
|
||||
exit "$exit_code"
|
||||
fi
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Cleanup temporary resources.
|
||||
#######################################
|
||||
cleanup() {
|
||||
if [ -n "${TEMP_BACKUP_DIR:-}" ] && [ -d "${TEMP_BACKUP_DIR}" ]; then
|
||||
rm -rf "${TEMP_BACKUP_DIR}"
|
||||
log "Removed temporary directory ${TEMP_BACKUP_DIR}"
|
||||
fi
|
||||
if [ -n "${TEMP_RESTORE_DIR:-}" ] && [ -d "${TEMP_RESTORE_DIR}" ]; then
|
||||
rm -rf "${TEMP_RESTORE_DIR}"
|
||||
log "Removed temporary directory ${TEMP_RESTORE_DIR}"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
#######################################
|
||||
# Main routine.
|
||||
#######################################
|
||||
main() {
|
||||
case "$OPERATION_MODE" in
|
||||
backup)
|
||||
case "$BACKUP_MODE" in
|
||||
directory)
|
||||
backup_directory
|
||||
;;
|
||||
postgres)
|
||||
backup_postgres
|
||||
;;
|
||||
s3)
|
||||
backup_s3
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
restore)
|
||||
case "$BACKUP_MODE" in
|
||||
directory)
|
||||
restore_directory
|
||||
;;
|
||||
postgres)
|
||||
restore_postgres
|
||||
;;
|
||||
s3)
|
||||
restore_s3
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Trap termination signals to log and exit cleanly.
|
||||
trap 'log "Script interrupted. Exiting."; exit 1' SIGINT SIGTERM
|
||||
|
||||
main
|
||||
Reference in New Issue
Block a user