Compare commits
16 Commits
6db128c80a
...
0.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 }}
|
||||||
24
.gitea/workflows/ci.yaml
Normal file
24
.gitea/workflows/ci.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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
|
||||||
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM alpine:3.22
|
||||||
|
|
||||||
|
RUN apk update && apk add --no-cache \
|
||||||
|
bash \
|
||||||
|
curl \
|
||||||
|
restic \
|
||||||
|
postgresql-client \
|
||||||
|
jq
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY src/backup.sh /app/backup.sh
|
||||||
|
|
||||||
|
RUN chmod +x /app/backup.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/backup.sh"]
|
||||||
114
README.md
114
README.md
@@ -1,3 +1,113 @@
|
|||||||
# backupsidecar
|
# BackupSidecar
|
||||||
|
|
||||||
Backup sidecar that automatically creates backups of one PVC and saves it to another PVC via restic
|
BackupSidecar is a lightweight backup solution designed to run as a cron job in Kubernetes. It automates backups using Restic and supports both directory and PostgreSQL database backups. Notifications are sent via Gotify to keep you informed of backup results.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
BackupSidecar is configured through environment variables. Below is a breakdown of the available settings.
|
||||||
|
|
||||||
|
### General Settings
|
||||||
|
|
||||||
|
These variables apply to both directory and PostgreSQL backups.
|
||||||
|
|
||||||
|
- **`BACKUP_MODE`** _(optional)_ - Defines the backup type (`directory` or `postgres`). 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.
|
||||||
|
- **`GOTIFYHOST`** _(required)_ - The Gotify server URL.
|
||||||
|
- **`GOTIFYTOKEN`** _(required)_ - The API token for Gotify.
|
||||||
|
- **`GOTIFYTOPIC`** _(required)_ - The topic under which backup notifications will be sent.
|
||||||
|
|
||||||
|
### Directory Backup
|
||||||
|
|
||||||
|
When running in `directory` mode, the following variable must be set:
|
||||||
|
|
||||||
|
- **`SOURCEDIR`** _(required)_ - The path of the directory to be backed up.
|
||||||
|
|
||||||
|
### PostgreSQL Backup
|
||||||
|
|
||||||
|
For `postgres` mode, the following database-related variables are required:
|
||||||
|
|
||||||
|
- **`PGHOST`** _(required)_ - The hostname of the PostgreSQL server.
|
||||||
|
- **`PGDATABASE`** _(required)_ - The name of the database to back up.
|
||||||
|
- **`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.
|
||||||
|
- **`PG_DUMP_ARGS`** _(optional)_ - Additional flags for `pg_dump`.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Ensure the following commands are available in the container:
|
||||||
|
|
||||||
|
- `restic`
|
||||||
|
- `curl`
|
||||||
|
- `jq`
|
||||||
|
- `pg_dump` _(only required for `postgres` mode)_
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
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: 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notifications
|
||||||
|
|
||||||
|
The script sends success or failure notifications via Gotify.
|
||||||
|
|
||||||
|
Example success notification:
|
||||||
|
|
||||||
|
```
|
||||||
|
Backup successful. Snapshot 56ff6a909a44e01f67d2d88f9a76aa713d437809d7ed14a2361e28893f38befb: files new: 1, files changed: 0, data added: 1019 bytes in 0.277535184 sec
|
||||||
|
```
|
||||||
|
|||||||
3
renovate.json
Normal file
3
renovate.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||||
|
}
|
||||||
188
src/backup.sh
Normal file
188
src/backup.sh
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Determine backup mode from the environment only.
|
||||||
|
# Valid values: "directory" or "postgres".
|
||||||
|
# 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
|
||||||
|
REQUIRED_CMDS+=(pg_dump)
|
||||||
|
fi
|
||||||
|
|
||||||
|
for cmd in "${REQUIRED_CMDS[@]}"; do
|
||||||
|
if ! command -v "$cmd" &>/dev/null; then
|
||||||
|
echo "Error: Required command '$cmd' is not installed." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Validate common required environment variables.
|
||||||
|
#######################################
|
||||||
|
# Gotify notification settings.
|
||||||
|
: "${GOTIFYHOST:?Environment variable GOTIFYHOST is not set}"
|
||||||
|
: "${GOTIFYTOKEN:?Environment variable GOTIFYTOKEN is not set}"
|
||||||
|
: "${GOTIFYTOPIC:?Environment variable GOTIFYTOPIC is not set}"
|
||||||
|
|
||||||
|
# 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 mode-specific environment variables.
|
||||||
|
#######################################
|
||||||
|
case "$BACKUP_MODE" in
|
||||||
|
directory)
|
||||||
|
: "${SOURCEDIR:?Environment variable SOURCEDIR is not set (required for directory backup mode)}"
|
||||||
|
;;
|
||||||
|
postgres)
|
||||||
|
: "${PGHOST:?Environment variable PGHOST is not set (required for PostgreSQL backup mode)}"
|
||||||
|
: "${PGDATABASE:?Environment variable PGDATABASE is not set (required for PostgreSQL backup mode)}"
|
||||||
|
: "${PGUSER:?Environment variable PGUSER is not set (required for PostgreSQL backup mode)}"
|
||||||
|
# Optional: default PGPORT to 5432.
|
||||||
|
: "${PGPORT:=5432}"
|
||||||
|
if [ -z "${PGPASSWORD:-}" ]; then
|
||||||
|
echo "Warning: Environment variable PGPASSWORD is not set. pg_dump may fail if authentication is required."
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: Unknown backup mode '$BACKUP_MODE'. Valid modes are 'directory' and 'postgres'." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Build the Gotify URL.
|
||||||
|
#######################################
|
||||||
|
GOTIFYURL="${GOTIFYHOST}/message?token=${GOTIFYTOKEN}"
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# 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") - $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Send a notification via Gotify.
|
||||||
|
# Arguments:
|
||||||
|
# message: The message to send.
|
||||||
|
#######################################
|
||||||
|
send_notification() {
|
||||||
|
local message="$1"
|
||||||
|
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}"
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Main routine.
|
||||||
|
#######################################
|
||||||
|
main() {
|
||||||
|
case "$BACKUP_MODE" in
|
||||||
|
directory)
|
||||||
|
backup_directory
|
||||||
|
;;
|
||||||
|
postgres)
|
||||||
|
backup_postgres
|
||||||
|
;;
|
||||||
|
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