feature/multi-cloud (#14)

Co-authored-by: Danijel Simeunovic <danijel.simeunovic@fortedigital.com>
Reviewed-on: #14
This commit was merged in pull request #14.
This commit is contained in:
2026-04-24 08:48:53 +00:00
parent 65598c9297
commit 8505481291
102 changed files with 1715 additions and 147 deletions

23
scripts/backup/aws-s3.sh Normal file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
# AWS S3 backup upload (native AWS CLI)
# Uses: aws cli v2
# Env: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, S3_BUCKET
BACKUP_FILE="${1:?Usage: $0 <backup-file>}"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
KEY="gitea-dump-${TIMESTAMP}.zip"
echo "Uploading ${KEY}..."
aws s3 cp "$BACKUP_FILE" "s3://${S3_BUCKET}/${KEY}"
echo "Upload complete."
# Prune backups older than 7 days
echo "Pruning backups older than 7 days..."
CUTOFF=$(date -d '7 days ago' +%Y-%m-%dT%H:%M:%S 2>/dev/null || date -v-7d +%Y-%m-%dT%H:%M:%S)
aws s3api list-objects-v2 --bucket "${S3_BUCKET}" --query "Contents[?LastModified<'${CUTOFF}'].Key" --output text \
| tr '\t' '\n' \
| while read -r key; do
[ -n "$key" ] && aws s3 rm "s3://${S3_BUCKET}/${key}" && echo "Deleted: ${key}"
done
echo "Pruning complete."

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail
# Azure Blob Storage backup upload
# Uses: az cli
# Env: AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_KEY, AZURE_CONTAINER
BACKUP_FILE="${1:?Usage: $0 <backup-file>}"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
KEY="gitea-dump-${TIMESTAMP}.zip"
echo "Uploading ${KEY}..."
az storage blob upload \
--account-name "${AZURE_STORAGE_ACCOUNT}" \
--account-key "${AZURE_STORAGE_KEY}" \
--container-name "${AZURE_CONTAINER}" \
--name "${KEY}" \
--file "$BACKUP_FILE" \
--overwrite
echo "Upload complete."
# Prune backups older than 7 days
echo "Pruning backups older than 7 days..."
CUTOFF=$(date -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-7d +%Y-%m-%dT%H:%M:%SZ)
az storage blob list \
--account-name "${AZURE_STORAGE_ACCOUNT}" \
--account-key "${AZURE_STORAGE_KEY}" \
--container-name "${AZURE_CONTAINER}" \
--query "[?properties.lastModified<'${CUTOFF}'].name" -o tsv \
| while read -r name; do
[ -n "$name" ] && az storage blob delete \
--account-name "${AZURE_STORAGE_ACCOUNT}" \
--account-key "${AZURE_STORAGE_KEY}" \
--container-name "${AZURE_CONTAINER}" \
--name "$name" && echo "Deleted: ${name}"
done
echo "Pruning complete."

26
scripts/backup/gcp-gcs.sh Normal file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
# GCP Cloud Storage backup upload
# Uses: gsutil (gcloud SDK)
# Env: GCS_BUCKET (e.g. gs://my-bucket)
BACKUP_FILE="${1:?Usage: $0 <backup-file>}"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
KEY="gitea-dump-${TIMESTAMP}.zip"
echo "Uploading ${KEY}..."
gsutil cp "$BACKUP_FILE" "${GCS_BUCKET}/${KEY}"
echo "Upload complete."
# Prune backups older than 7 days — GCS lifecycle rules are preferred,
# but this works as a manual fallback
echo "Pruning backups older than 7 days..."
CUTOFF=$(date -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-7d +%Y-%m-%dT%H:%M:%SZ)
gsutil ls -l "${GCS_BUCKET}/" \
| grep 'gitea-dump-' \
| while read -r size date name; do
if [[ "$date" < "$CUTOFF" ]]; then
gsutil rm "$name" && echo "Deleted: ${name}"
fi
done
echo "Pruning complete."

View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
# S3-compatible backup upload (UpCloud Objects, MinIO, Wasabi, etc.)
# Uses: minio/mc
# Env: S3_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET
BACKUP_FILE="${1:?Usage: $0 <backup-file>}"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
KEY="gitea-dump-${TIMESTAMP}.zip"
mc alias set s3 "${S3_ENDPOINT}" "${AWS_ACCESS_KEY_ID}" "${AWS_SECRET_ACCESS_KEY}"
echo "Uploading ${KEY}..."
mc cp "$BACKUP_FILE" "s3/${S3_BUCKET}/${KEY}"
echo "Upload complete."
# Prune backups older than 7 days
echo "Pruning backups older than 7 days..."
mc rm --older-than 7d --force "s3/${S3_BUCKET}/" 2>&1 || true
echo "Pruning complete."

100
scripts/gitea-backup-aks.sh Normal file
View File

@@ -0,0 +1,100 @@
#!/usr/bin/env bash
set -euo pipefail
# Gitea backup helper for Azure Blob Storage
# Uses the gitea-backup-aks secret in the gitea namespace
# Required secret keys:
# AZURE_STORAGE_ACCOUNT — storage account name
# AZURE_STORAGE_KEY — storage account key
# AZURE_CONTAINER — blob container name
#
# Usage:
# ./scripts/gitea-backup-aks.sh list # list all backups
# ./scripts/gitea-backup-aks.sh download <filename> # download a backup
# ./scripts/gitea-backup-aks.sh download latest # download the most recent backup
NAMESPACE="gitea"
SECRET="gitea-backup-aks"
IMAGE="mcr.microsoft.com/azure-cli:latest"
POD_NAME="gitea-backup-helper"
cleanup() {
kubectl -n "$NAMESPACE" delete pod "$POD_NAME" --ignore-not-found --grace-period=0 > /dev/null 2>&1 || true
}
az_run() {
cleanup
kubectl -n "$NAMESPACE" run "$POD_NAME" --restart=Never \
--image="$IMAGE" \
--overrides="{
\"spec\":{\"containers\":[{
\"name\":\"$POD_NAME\",
\"image\":\"$IMAGE\",
\"env\":[{\"name\":\"HOME\",\"value\":\"/tmp\"}],
\"command\":[\"sh\",\"-c\",\"$1\"],
\"envFrom\":[{\"secretRef\":{\"name\":\"$SECRET\"}}]
}]}
}" > /dev/null 2>&1
kubectl -n "$NAMESPACE" wait --for=jsonpath='{.status.phase}'=Succeeded "pod/$POD_NAME" --timeout=120s > /dev/null 2>&1
kubectl -n "$NAMESPACE" logs "$POD_NAME"
cleanup
}
case "${1:-help}" in
list)
echo "Listing backups..."
az_run 'az storage blob list --account-name ${AZURE_STORAGE_ACCOUNT} --account-key ${AZURE_STORAGE_KEY} --container-name ${AZURE_CONTAINER} --output table --query "[].{Name:name, Size:properties.contentLength, Modified:properties.lastModified}"'
;;
download)
FILE="${2:?Usage: $0 download <filename|latest>}"
if [ "$FILE" = "latest" ]; then
echo "Finding latest backup..."
FILE=$(az_run 'az storage blob list --account-name ${AZURE_STORAGE_ACCOUNT} --account-key ${AZURE_STORAGE_KEY} --container-name ${AZURE_CONTAINER} --query "sort_by([], &properties.lastModified)[-1].name" -o tsv' | tr -d '[:space:]')
if [ -z "$FILE" ]; then
echo "No backups found."
exit 1
fi
echo "Latest: $FILE"
fi
echo "Downloading $FILE..."
cleanup
kubectl -n "$NAMESPACE" run "$POD_NAME" --restart=Never \
--image="$IMAGE" \
--overrides="{
\"spec\":{\"containers\":[{
\"name\":\"$POD_NAME\",
\"image\":\"$IMAGE\",
\"env\":[{\"name\":\"HOME\",\"value\":\"/tmp\"}],
\"command\":[\"sh\",\"-c\",\"sleep 300\"],
\"envFrom\":[{\"secretRef\":{\"name\":\"$SECRET\"}}]
}]}
}" > /dev/null 2>&1
kubectl -n "$NAMESPACE" wait --for=condition=Ready "pod/$POD_NAME" --timeout=60s > /dev/null 2>&1
echo "Saving to ./$FILE ..."
kubectl -n "$NAMESPACE" exec "$POD_NAME" -- \
az storage blob download \
--account-name "\${AZURE_STORAGE_ACCOUNT}" \
--account-key "\${AZURE_STORAGE_KEY}" \
--container-name "\${AZURE_CONTAINER}" \
--name "$FILE" \
--file /dev/stdout 2>/dev/null > "./$FILE"
cleanup
echo "Downloaded: ./$FILE"
;;
*)
echo "Gitea backup helper (Azure Blob Storage)"
echo ""
echo "Usage:"
echo " $0 list List all backups in Azure Blob"
echo " $0 download <filename> Download a specific backup"
echo " $0 download latest Download the most recent backup"
;;
esac

View File

@@ -0,0 +1,94 @@
#!/usr/bin/env bash
set -euo pipefail
# Gitea backup helper for AWS S3
# Uses the gitea-backup-s3 secret in the gitea namespace
# (same secret schema: S3_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET)
#
# For AWS, S3_ENDPOINT is typically https://s3.<region>.amazonaws.com
#
# Usage:
# ./scripts/gitea-backup-eks.sh list # list all backups
# ./scripts/gitea-backup-eks.sh download <filename> # download a backup to current dir
# ./scripts/gitea-backup-eks.sh download latest # download the most recent backup
NAMESPACE="gitea"
SECRET="gitea-backup-s3"
IMAGE="minio/mc:latest"
POD_NAME="gitea-backup-helper"
ALIAS_CMD='mc alias set s3 ${S3_ENDPOINT} ${AWS_ACCESS_KEY_ID} ${AWS_SECRET_ACCESS_KEY} > /dev/null'
cleanup() {
kubectl -n "$NAMESPACE" delete pod "$POD_NAME" --ignore-not-found --grace-period=0 > /dev/null 2>&1 || true
}
mc_run() {
cleanup
kubectl -n "$NAMESPACE" run "$POD_NAME" --restart=Never \
--image="$IMAGE" \
--overrides="{
\"spec\":{\"containers\":[{
\"name\":\"$POD_NAME\",
\"image\":\"$IMAGE\",
\"env\":[{\"name\":\"HOME\",\"value\":\"/tmp\"}],
\"command\":[\"sh\",\"-c\",\"${ALIAS_CMD}; $1\"],
\"envFrom\":[{\"secretRef\":{\"name\":\"$SECRET\"}}]
}]}
}" > /dev/null 2>&1
kubectl -n "$NAMESPACE" wait --for=jsonpath='{.status.phase}'=Succeeded "pod/$POD_NAME" --timeout=120s > /dev/null 2>&1
kubectl -n "$NAMESPACE" logs "$POD_NAME"
cleanup
}
case "${1:-help}" in
list)
echo "Listing backups..."
mc_run 'mc ls s3/${S3_BUCKET}/'
;;
download)
FILE="${2:?Usage: $0 download <filename|latest>}"
if [ "$FILE" = "latest" ]; then
echo "Finding latest backup..."
FILE=$(mc_run 'mc ls s3/${S3_BUCKET}/' | sort | tail -1 | awk '{print $NF}' | tr -d '[:space:]')
if [ -z "$FILE" ]; then
echo "No backups found."
exit 1
fi
echo "Latest: $FILE"
fi
echo "Downloading $FILE..."
cleanup
kubectl -n "$NAMESPACE" run "$POD_NAME" --restart=Never \
--image="$IMAGE" \
--overrides="{
\"spec\":{\"containers\":[{
\"name\":\"$POD_NAME\",
\"image\":\"$IMAGE\",
\"env\":[{\"name\":\"HOME\",\"value\":\"/tmp\"}],
\"command\":[\"sh\",\"-c\",\"sleep 300\"],
\"envFrom\":[{\"secretRef\":{\"name\":\"$SECRET\"}}]
}]}
}" > /dev/null 2>&1
kubectl -n "$NAMESPACE" wait --for=condition=Ready "pod/$POD_NAME" --timeout=60s > /dev/null 2>&1
echo "Saving to ./$FILE ..."
kubectl -n "$NAMESPACE" exec "$POD_NAME" -- sh -c "${ALIAS_CMD} && mc cat s3/\${S3_BUCKET}/$FILE" > "./$FILE"
cleanup
echo "Downloaded: ./$FILE"
;;
*)
echo "Gitea backup helper (AWS S3)"
echo ""
echo "Usage:"
echo " $0 list List all backups in S3"
echo " $0 download <filename> Download a specific backup"
echo " $0 download latest Download the most recent backup"
;;
esac

View File

@@ -0,0 +1,95 @@
#!/usr/bin/env bash
set -euo pipefail
# Gitea backup helper for Google Cloud Storage
# Uses the gitea-backup-gcs secret in the gitea namespace
# Required secret keys:
# GCS_BUCKET — bucket name (without gs:// prefix)
# GOOGLE_APPLICATION_CREDENTIALS_JSON — service account key JSON
# (alternatively, use Workload Identity and omit the key)
#
# Usage:
# ./scripts/gitea-backup-gke.sh list # list all backups
# ./scripts/gitea-backup-gke.sh download <filename> # download a backup
# ./scripts/gitea-backup-gke.sh download latest # download the most recent backup
NAMESPACE="gitea"
SECRET="gitea-backup-gcs"
IMAGE="gcr.io/google.com/cloudsdktool/google-cloud-cli:slim"
POD_NAME="gitea-backup-helper"
AUTH_CMD='if [ -n "${GOOGLE_APPLICATION_CREDENTIALS_JSON:-}" ]; then echo "${GOOGLE_APPLICATION_CREDENTIALS_JSON}" > /tmp/gcs-key.json && gcloud auth activate-service-account --key-file=/tmp/gcs-key.json > /dev/null 2>&1; fi'
cleanup() {
kubectl -n "$NAMESPACE" delete pod "$POD_NAME" --ignore-not-found --grace-period=0 > /dev/null 2>&1 || true
}
gcs_run() {
cleanup
kubectl -n "$NAMESPACE" run "$POD_NAME" --restart=Never \
--image="$IMAGE" \
--overrides="{
\"spec\":{\"containers\":[{
\"name\":\"$POD_NAME\",
\"image\":\"$IMAGE\",
\"env\":[{\"name\":\"HOME\",\"value\":\"/tmp\"}],
\"command\":[\"sh\",\"-c\",\"${AUTH_CMD}; $1\"],
\"envFrom\":[{\"secretRef\":{\"name\":\"$SECRET\"}}]
}]}
}" > /dev/null 2>&1
kubectl -n "$NAMESPACE" wait --for=jsonpath='{.status.phase}'=Succeeded "pod/$POD_NAME" --timeout=120s > /dev/null 2>&1
kubectl -n "$NAMESPACE" logs "$POD_NAME"
cleanup
}
case "${1:-help}" in
list)
echo "Listing backups..."
gcs_run 'gsutil ls -l gs://${GCS_BUCKET}/'
;;
download)
FILE="${2:?Usage: $0 download <filename|latest>}"
if [ "$FILE" = "latest" ]; then
echo "Finding latest backup..."
FILE=$(gcs_run 'gsutil ls gs://${GCS_BUCKET}/' | grep -v '^$' | grep -v 'TOTAL' | sort | tail -1 | xargs -I{} basename {} | tr -d '[:space:]')
if [ -z "$FILE" ]; then
echo "No backups found."
exit 1
fi
echo "Latest: $FILE"
fi
echo "Downloading $FILE..."
cleanup
kubectl -n "$NAMESPACE" run "$POD_NAME" --restart=Never \
--image="$IMAGE" \
--overrides="{
\"spec\":{\"containers\":[{
\"name\":\"$POD_NAME\",
\"image\":\"$IMAGE\",
\"env\":[{\"name\":\"HOME\",\"value\":\"/tmp\"}],
\"command\":[\"sh\",\"-c\",\"sleep 300\"],
\"envFrom\":[{\"secretRef\":{\"name\":\"$SECRET\"}}]
}]}
}" > /dev/null 2>&1
kubectl -n "$NAMESPACE" wait --for=condition=Ready "pod/$POD_NAME" --timeout=60s > /dev/null 2>&1
echo "Saving to ./$FILE ..."
kubectl -n "$NAMESPACE" exec "$POD_NAME" -- sh -c "${AUTH_CMD} && gsutil cat gs://\${GCS_BUCKET}/$FILE" > "./$FILE"
cleanup
echo "Downloaded: ./$FILE"
;;
*)
echo "Gitea backup helper (Google Cloud Storage)"
echo ""
echo "Usage:"
echo " $0 list List all backups in GCS"
echo " $0 download <filename> Download a specific backup"
echo " $0 download latest Download the most recent backup"
;;
esac

View File

@@ -13,7 +13,7 @@ NAMESPACE="gitea"
SECRET="gitea-backup-s3"
IMAGE="minio/mc:latest"
POD_NAME="gitea-backup-helper"
ALIAS_CMD='mc alias set upcloud ${S3_ENDPOINT} ${AWS_ACCESS_KEY_ID} ${AWS_SECRET_ACCESS_KEY} > /dev/null'
ALIAS_CMD='mc alias set s3 ${S3_ENDPOINT} ${AWS_ACCESS_KEY_ID} ${AWS_SECRET_ACCESS_KEY} > /dev/null'
cleanup() {
kubectl -n "$NAMESPACE" delete pod "$POD_NAME" --ignore-not-found --grace-period=0 > /dev/null 2>&1 || true
@@ -41,7 +41,7 @@ mc_run() {
case "${1:-help}" in
list)
echo "Listing backups..."
mc_run 'mc ls upcloud/${S3_BUCKET}/'
mc_run 'mc ls s3/${S3_BUCKET}/'
;;
download)
@@ -49,7 +49,7 @@ case "${1:-help}" in
if [ "$FILE" = "latest" ]; then
echo "Finding latest backup..."
FILE=$(mc_run 'mc ls upcloud/${S3_BUCKET}/' | sort | tail -1 | awk '{print $NF}' | tr -d '[:space:]')
FILE=$(mc_run 'mc ls s3/${S3_BUCKET}/' | sort | tail -1 | awk '{print $NF}' | tr -d '[:space:]')
if [ -z "$FILE" ]; then
echo "No backups found."
exit 1
@@ -74,7 +74,7 @@ case "${1:-help}" in
kubectl -n "$NAMESPACE" wait --for=condition=Ready "pod/$POD_NAME" --timeout=60s > /dev/null 2>&1
echo "Saving to ./$FILE ..."
kubectl -n "$NAMESPACE" exec "$POD_NAME" -- sh -c "${ALIAS_CMD} && mc cat upcloud/\${S3_BUCKET}/$FILE" > "./$FILE"
kubectl -n "$NAMESPACE" exec "$POD_NAME" -- sh -c "${ALIAS_CMD} && mc cat s3/\${S3_BUCKET}/$FILE" > "./$FILE"
cleanup
echo "Downloaded: ./$FILE"