migration
This commit is contained in:
@@ -334,6 +334,7 @@ kubectl patch application myapp -n argocd \
|
|||||||
| **Loki** | Logs | `monitoring` | 1 |
|
| **Loki** | Logs | `monitoring` | 1 |
|
||||||
| **Tempo** | Distributed tracing | `monitoring` | 1 |
|
| **Tempo** | Distributed tracing | `monitoring` | 1 |
|
||||||
| **Fluent-Bit** | Log shipping | `monitoring` | DaemonSet |
|
| **Fluent-Bit** | Log shipping | `monitoring` | DaemonSet |
|
||||||
|
| **OpenCost** | Cost monitoring | `monitoring` | 1 |
|
||||||
| **Trivy** | Vulnerability scanning | `trivy-system` | 1 |
|
| **Trivy** | Vulnerability scanning | `trivy-system` | 1 |
|
||||||
|
|
||||||
**Full specs**: [Technical Reference - Infrastructure Components](docs/REFERENCE.md#infrastructure-components)
|
**Full specs**: [Technical Reference - Infrastructure Components](docs/REFERENCE.md#infrastructure-components)
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: default
|
project: default
|
||||||
sources:
|
sources:
|
||||||
- repoURL: git@github.com:fortedigital/forte-helm
|
- repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git
|
||||||
path: forteapp
|
path: forteapp
|
||||||
targetRevision: HEAD
|
targetRevision: HEAD
|
||||||
helm:
|
helm:
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- $values/argocd-mcp/values.yaml
|
- $values/argocd-mcp/values.yaml
|
||||||
|
|
||||||
- repoURL: git@github.com:fortedigital/helm-values.git
|
- repoURL: ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git
|
||||||
targetRevision: HEAD
|
targetRevision: HEAD
|
||||||
ref: values
|
ref: values
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: default
|
project: default
|
||||||
sources:
|
sources:
|
||||||
- repoURL: git@github.com:fortedigital/forte-helm
|
- repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git
|
||||||
path: forteapp
|
path: forteapp
|
||||||
targetRevision: HEAD
|
targetRevision: HEAD
|
||||||
helm:
|
helm:
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- $values/mcp10x/values.yaml
|
- $values/mcp10x/values.yaml
|
||||||
|
|
||||||
- repoURL: git@github.com:fortedigital/helm-values.git
|
- repoURL: ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git
|
||||||
targetRevision: HEAD
|
targetRevision: HEAD
|
||||||
ref: values
|
ref: values
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: default
|
project: default
|
||||||
sources:
|
sources:
|
||||||
- repoURL: git@github.com:fortedigital/forte-helm
|
- repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git
|
||||||
path: forteapp
|
path: forteapp
|
||||||
targetRevision: HEAD
|
targetRevision: HEAD
|
||||||
helm:
|
helm:
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- $values/musicman/values.yaml
|
- $values/musicman/values.yaml
|
||||||
|
|
||||||
- repoURL: git@github.com:fortedigital/helm-values.git
|
- repoURL: ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git
|
||||||
targetRevision: HEAD
|
targetRevision: HEAD
|
||||||
ref: values
|
ref: values
|
||||||
|
|
||||||
|
|||||||
88
cluster-resources/gitea-backup-cronjob.yaml
Normal file
88
cluster-resources/gitea-backup-cronjob.yaml
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
apiVersion: batch/v1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: gitea-backup
|
||||||
|
namespace: gitea
|
||||||
|
spec:
|
||||||
|
schedule: "0 3 * * *" # daily at 03:00 UTC
|
||||||
|
concurrencyPolicy: Forbid
|
||||||
|
successfulJobsHistoryLimit: 3
|
||||||
|
failedJobsHistoryLimit: 3
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
backoffLimit: 1
|
||||||
|
activeDeadlineSeconds: 1800
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 1000
|
||||||
|
runAsGroup: 1000
|
||||||
|
fsGroup: 1000
|
||||||
|
# Must run on the same node as Gitea to share the RWO volume
|
||||||
|
affinity:
|
||||||
|
podAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
- labelSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: gitea
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
initContainers:
|
||||||
|
- name: gitea-dump
|
||||||
|
image: gitea/gitea:1.25.4
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
gitea dump \
|
||||||
|
-c /data/gitea/conf/app.ini \
|
||||||
|
-f /backup/gitea-dump.zip \
|
||||||
|
-t /tmp/gitea-dump && \
|
||||||
|
echo "Dump completed: $(ls -lh /backup/gitea-dump.zip)"
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /data
|
||||||
|
readOnly: true
|
||||||
|
- name: backup
|
||||||
|
mountPath: /backup
|
||||||
|
- name: tmp
|
||||||
|
mountPath: /tmp/gitea-dump
|
||||||
|
containers:
|
||||||
|
- name: upload
|
||||||
|
image: minio/mc:latest
|
||||||
|
env:
|
||||||
|
- name: HOME
|
||||||
|
value: /tmp
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
mc alias set upcloud "${S3_ENDPOINT}" "${AWS_ACCESS_KEY_ID}" "${AWS_SECRET_ACCESS_KEY}"
|
||||||
|
|
||||||
|
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||||
|
KEY="gitea-dump-${TIMESTAMP}.zip"
|
||||||
|
echo "Uploading ${KEY}..."
|
||||||
|
mc cp /backup/gitea-dump.zip "upcloud/${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 "upcloud/${S3_BUCKET}/" 2>&1 || true
|
||||||
|
echo "Pruning complete."
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: gitea-backup-s3
|
||||||
|
volumeMounts:
|
||||||
|
- name: backup
|
||||||
|
mountPath: /backup
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: gitea-shared-storage
|
||||||
|
- name: backup
|
||||||
|
emptyDir:
|
||||||
|
sizeLimit: 5Gi
|
||||||
|
- name: tmp
|
||||||
|
emptyDir:
|
||||||
|
sizeLimit: 5Gi
|
||||||
13
cluster-resources/gitea-ssh-ingressroute.yaml
Normal file
13
cluster-resources/gitea-ssh-ingressroute.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRouteTCP
|
||||||
|
metadata:
|
||||||
|
name: gitea-ssh
|
||||||
|
namespace: gitea
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- giteassh
|
||||||
|
routes:
|
||||||
|
- match: HostSNI(`*`)
|
||||||
|
services:
|
||||||
|
- name: gitea-ssh
|
||||||
|
port: 22
|
||||||
@@ -127,7 +127,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: authn
|
- name: authn
|
||||||
image: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image\" || 'ghcr.io/fortedigital/auth-sidecar' }}:{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image-version\" || 'latest' }}"
|
image: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image\" || 'git.forteapps.net/forte/auth-sidecar' }}:{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image-version\" || 'latest' }}"
|
||||||
ports:
|
ports:
|
||||||
- containerPort: "{{ sidecarPort }}"
|
- containerPort: "{{ sidecarPort }}"
|
||||||
name: auth
|
name: auth
|
||||||
@@ -208,7 +208,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: authn
|
- name: authn
|
||||||
image: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image\" || 'ghcr.io/fortedigital/auth-sidecar' }}:{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image-version\" || 'latest' }}"
|
image: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image\" || 'git.forteapps.net/forte/auth-sidecar' }}:{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image-version\" || 'latest' }}"
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: "{{ sidecarPort }}"
|
- containerPort: "{{ sidecarPort }}"
|
||||||
@@ -301,7 +301,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: authn
|
- name: authn
|
||||||
image: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image\" || 'ghcr.io/fortedigital/auth-sidecar' }}:{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image-version\" || 'latest' }}"
|
image: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image\" || 'git.forteapps.net/forte/auth-sidecar' }}:{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image-version\" || 'latest' }}"
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: "{{ sidecarPort }}"
|
- containerPort: "{{ sidecarPort }}"
|
||||||
@@ -380,7 +380,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: authn
|
- name: authn
|
||||||
image: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image\" || 'ghcr.io/fortedigital/auth-sidecar' }}:{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image-version\" || 'latest' }}"
|
image: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image\" || 'git.forteapps.net/forte/auth-sidecar' }}:{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-image-version\" || 'latest' }}"
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: "{{ sidecarPort }}"
|
- containerPort: "{{ sidecarPort }}"
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ Save the following file in private/ (gitignored) folder as secret.yaml
|
|||||||
argocd.argoproj.io/secret-type: repository
|
argocd.argoproj.io/secret-type: repository
|
||||||
stringData:
|
stringData:
|
||||||
type: git
|
type: git
|
||||||
url: git@github.com:fortedigital/forte-helm.git
|
url: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git
|
||||||
sshPrivateKey: |
|
sshPrivateKey: |
|
||||||
<paste your private key here>
|
<paste your private key here>
|
||||||
project: default
|
project: default
|
||||||
|
|||||||
@@ -40,3 +40,9 @@ spec:
|
|||||||
- CreateNamespace=true
|
- CreateNamespace=true
|
||||||
- Validate=true
|
- Validate=true
|
||||||
- ServerSideApply=true
|
- ServerSideApply=true
|
||||||
|
|
||||||
|
ignoreDifferences:
|
||||||
|
- group: apps
|
||||||
|
kind: StatefulSet
|
||||||
|
jsonPointers:
|
||||||
|
- /spec/volumeClaimTemplates
|
||||||
|
|||||||
@@ -40,3 +40,9 @@ spec:
|
|||||||
- CreateNamespace=true
|
- CreateNamespace=true
|
||||||
- Validate=true
|
- Validate=true
|
||||||
- ServerSideApply=true
|
- ServerSideApply=true
|
||||||
|
|
||||||
|
ignoreDifferences:
|
||||||
|
- group: apps
|
||||||
|
kind: StatefulSet
|
||||||
|
jsonPointers:
|
||||||
|
- /spec/volumeClaimTemplates
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ spec:
|
|||||||
{
|
{
|
||||||
"name": "websecure",
|
"name": "websecure",
|
||||||
"mode": "tcp"
|
"mode": "tcp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "giteassh",
|
||||||
|
"mode": "tcp"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"backends": [
|
"backends": [
|
||||||
@@ -90,6 +94,9 @@ spec:
|
|||||||
"properties": {
|
"properties": {
|
||||||
"outbound_proxy_protocol": "v2"
|
"outbound_proxy_protocol": "v2"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "giteassh"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -129,6 +136,13 @@ spec:
|
|||||||
metrics: true
|
metrics: true
|
||||||
tracing: true
|
tracing: true
|
||||||
|
|
||||||
|
giteassh:
|
||||||
|
port: 2222
|
||||||
|
expose:
|
||||||
|
default: true
|
||||||
|
exposedPort: 2222
|
||||||
|
protocol: TCP
|
||||||
|
|
||||||
destination:
|
destination:
|
||||||
server: https://kubernetes.default.svc
|
server: https://kubernetes.default.svc
|
||||||
namespace: traefik-system
|
namespace: traefik-system
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
giteaRootURL: http://gitea-http.gitea.svc.cluster.local:3000
|
giteaRootURL: https://git.forteapps.net
|
||||||
|
|
||||||
existingSecret: gitea-runner-token
|
existingSecret: gitea-runner-token
|
||||||
existingSecretKey: token
|
existingSecretKey: token
|
||||||
@@ -30,8 +30,7 @@ statefulset:
|
|||||||
docker_timeout: 300s
|
docker_timeout: 300s
|
||||||
runner:
|
runner:
|
||||||
labels:
|
labels:
|
||||||
- "ubuntu-latest:docker://node:20-bookworm"
|
- "ubuntu-latest:docker://catthehacker/ubuntu:act-22.04"
|
||||||
- "ubuntu-22.04:docker://node:20-bookworm"
|
- "ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04"
|
||||||
|
|
||||||
dind:
|
dind:
|
||||||
rootless: false
|
rootless: false
|
||||||
|
|||||||
@@ -17,13 +17,17 @@ gitea:
|
|||||||
DOMAIN: git.forteapps.net
|
DOMAIN: git.forteapps.net
|
||||||
ROOT_URL: https://git.forteapps.net
|
ROOT_URL: https://git.forteapps.net
|
||||||
SSH_DOMAIN: git.forteapps.net
|
SSH_DOMAIN: git.forteapps.net
|
||||||
SSH_PORT: 22
|
SSH_PORT: 2222
|
||||||
LFS_START_SERVER: true
|
LFS_START_SERVER: true
|
||||||
|
ENABLE_GITEA_PAGES: true
|
||||||
|
|
||||||
service:
|
service:
|
||||||
DISABLE_REGISTRATION: false
|
DISABLE_REGISTRATION: false
|
||||||
|
DEFAULT_ALLOW_CREATE_ORGANIZATION: false
|
||||||
REQUIRE_SIGNIN_VIEW: false
|
REQUIRE_SIGNIN_VIEW: false
|
||||||
ALLOW_ONLY_EXTERNAL_REGISTRATION: true
|
ALLOW_ONLY_EXTERNAL_REGISTRATION: true
|
||||||
|
ENABLE_BASIC_AUTHENTICATION: true
|
||||||
|
ENABLE_PASSWORD_SIGNIN_FORM: false
|
||||||
|
|
||||||
openid:
|
openid:
|
||||||
ENABLE_OPENID_SIGNIN: false
|
ENABLE_OPENID_SIGNIN: false
|
||||||
@@ -67,8 +71,8 @@ gitea:
|
|||||||
existingSecret: gitea-credentials
|
existingSecret: gitea-credentials
|
||||||
key: gitea
|
key: gitea
|
||||||
autoDiscoverUrl: "https://id.forteapps.net/realms/forte/.well-known/openid-configuration"
|
autoDiscoverUrl: "https://id.forteapps.net/realms/forte/.well-known/openid-configuration"
|
||||||
scopes: "openid email profile"
|
scopes: "openid email profile organization"
|
||||||
groupClaimName: ""
|
groupClaimName: "groups"
|
||||||
adminGroup: ""
|
adminGroup: ""
|
||||||
restrictedGroup: ""
|
restrictedGroup: ""
|
||||||
# -- Prometheus metrics (scraped via annotations, no ServiceMonitor CRD needed)
|
# -- Prometheus metrics (scraped via annotations, no ServiceMonitor CRD needed)
|
||||||
@@ -146,7 +150,7 @@ redis-cluster:
|
|||||||
test:
|
test:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
# -- SSH service (ClusterIP for now; enable NodePort if SSH access needed)
|
# -- SSH service (ClusterIP, exposed externally via Traefik TCP IngressRoute on port 2222)
|
||||||
service:
|
service:
|
||||||
ssh:
|
ssh:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ opencost:
|
|||||||
provider: custom
|
provider: custom
|
||||||
costModel:
|
costModel:
|
||||||
description: "UpCloud 4-node cluster pricing"
|
description: "UpCloud 4-node cluster pricing"
|
||||||
CPU: "6.07"
|
CPU: "5.86"
|
||||||
RAM: "1.52"
|
RAM: "1.46"
|
||||||
GPU: "0"
|
GPU: "0"
|
||||||
storage: "0.03"
|
storage: "0.34"
|
||||||
zoneNetworkEgress: "0"
|
zoneNetworkEgress: "0"
|
||||||
regionNetworkEgress: "0"
|
regionNetworkEgress: "0"
|
||||||
internetNetworkEgress: "0"
|
internetNetworkEgress: "0"
|
||||||
|
|||||||
91
scripts/gitea-backup.sh
Normal file
91
scripts/gitea-backup.sh
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Gitea backup helper — interacts with the S3 bucket via a temporary pod
|
||||||
|
# Uses the gitea-backup-s3 secret in the gitea namespace
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/gitea-backup.sh list # list all backups
|
||||||
|
# ./scripts/gitea-backup.sh download <filename> # download a backup to current dir
|
||||||
|
# ./scripts/gitea-backup.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 upcloud ${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 upcloud/${S3_BUCKET}/'
|
||||||
|
;;
|
||||||
|
|
||||||
|
download)
|
||||||
|
FILE="${2:?Usage: $0 download <filename|latest>}"
|
||||||
|
|
||||||
|
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:]')
|
||||||
|
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 upcloud/\${S3_BUCKET}/$FILE" > "./$FILE"
|
||||||
|
cleanup
|
||||||
|
|
||||||
|
echo "Downloaded: ./$FILE"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Gitea backup helper"
|
||||||
|
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
|
||||||
165
scripts/gitea-restore.sh
Normal file
165
scripts/gitea-restore.sh
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Gitea restore helper — restores a gitea-dump zip into a running Gitea deployment
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - Gitea deployed on the target cluster (Helm chart via ArgoCD)
|
||||||
|
# - kubectl context pointing to the target cluster
|
||||||
|
# - A gitea-dump zip file (from gitea-backup.sh download or CronJob)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/gitea-restore.sh <gitea-dump-file.zip>
|
||||||
|
#
|
||||||
|
# What it does:
|
||||||
|
# 1. Scales Gitea down to 0
|
||||||
|
# 2. Restores the PostgreSQL database from gitea-db.sql
|
||||||
|
# 3. Restores Git repositories to the data PVC
|
||||||
|
# 4. Scales Gitea back up
|
||||||
|
|
||||||
|
NAMESPACE="gitea"
|
||||||
|
DEPLOYMENT="gitea"
|
||||||
|
PG_STATEFULSET="gitea-postgresql"
|
||||||
|
PVC="gitea-shared-storage"
|
||||||
|
HELPER_POD="gitea-restore-helper"
|
||||||
|
HELPER_IMAGE="alpine:3.20"
|
||||||
|
PG_USER="gitea"
|
||||||
|
PG_DB="gitea"
|
||||||
|
|
||||||
|
DUMP_FILE="${1:?Usage: $0 <gitea-dump-file.zip>}"
|
||||||
|
|
||||||
|
if [ ! -f "$DUMP_FILE" ]; then
|
||||||
|
echo "Error: file not found: $DUMP_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Gitea Restore ==="
|
||||||
|
echo "Dump file: $DUMP_FILE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# --- Safety prompt ---
|
||||||
|
read -r -p "This will OVERWRITE the Gitea database and repositories on the current cluster. Continue? [y/N] " confirm
|
||||||
|
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
kubectl -n "$NAMESPACE" delete pod "$HELPER_POD" --ignore-not-found --grace-period=0 > /dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# --- Step 1: Extract dump locally ---
|
||||||
|
RESTORE_DIR=$(mktemp -d)
|
||||||
|
echo ""
|
||||||
|
echo "[1/5] Extracting dump..."
|
||||||
|
unzip -q "$DUMP_FILE" -d "$RESTORE_DIR"
|
||||||
|
|
||||||
|
# Detect SQL dump file (gitea-db.sql or gitea-dump-*.sql)
|
||||||
|
SQL_FILE=$(find "$RESTORE_DIR" -maxdepth 1 -name '*.sql' | head -1)
|
||||||
|
if [ -z "$SQL_FILE" ]; then
|
||||||
|
echo "Error: no .sql file found in dump."
|
||||||
|
rm -rf "$RESTORE_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect repos — either a "repos" folder or gitea-repo.zip
|
||||||
|
if [ -d "$RESTORE_DIR/repos" ]; then
|
||||||
|
REPOS_SOURCE="dir"
|
||||||
|
REPOS_PATH="$RESTORE_DIR/repos"
|
||||||
|
elif [ -f "$RESTORE_DIR/gitea-repo.zip" ]; then
|
||||||
|
REPOS_SOURCE="zip"
|
||||||
|
REPOS_PATH="$RESTORE_DIR/gitea-repo.zip"
|
||||||
|
else
|
||||||
|
echo "Error: no repos/ directory or gitea-repo.zip found in dump."
|
||||||
|
rm -rf "$RESTORE_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Found: $(basename "$SQL_FILE") ($(du -h "$SQL_FILE" | cut -f1))"
|
||||||
|
echo " Found: repos ($REPOS_SOURCE)"
|
||||||
|
[ -d "$RESTORE_DIR/data" ] && echo " Found: data/"
|
||||||
|
[ -f "$RESTORE_DIR/app.ini" ] && echo " Found: app.ini (managed by Helm, skipping)"
|
||||||
|
|
||||||
|
# --- Step 2: Scale down Gitea ---
|
||||||
|
echo ""
|
||||||
|
echo "[2/5] Scaling down Gitea..."
|
||||||
|
kubectl -n "$NAMESPACE" scale deployment "$DEPLOYMENT" --replicas=0
|
||||||
|
kubectl -n "$NAMESPACE" rollout status deployment "$DEPLOYMENT" --timeout=60s 2>/dev/null || true
|
||||||
|
echo " Waiting for pods to terminate..."
|
||||||
|
kubectl -n "$NAMESPACE" wait --for=delete pod -l app.kubernetes.io/name=gitea --timeout=120s 2>/dev/null || true
|
||||||
|
echo " Gitea is down."
|
||||||
|
|
||||||
|
# --- Step 3: Restore database ---
|
||||||
|
echo ""
|
||||||
|
echo "[3/5] Restoring database..."
|
||||||
|
# Drop and recreate to ensure clean state
|
||||||
|
kubectl -n "$NAMESPACE" exec "${PG_STATEFULSET}-0" -- \
|
||||||
|
psql -U "$PG_USER" -d postgres -c "DROP DATABASE IF EXISTS ${PG_DB};" 2>/dev/null
|
||||||
|
kubectl -n "$NAMESPACE" exec "${PG_STATEFULSET}-0" -- \
|
||||||
|
psql -U "$PG_USER" -d postgres -c "CREATE DATABASE ${PG_DB} OWNER ${PG_USER};" 2>/dev/null
|
||||||
|
kubectl -n "$NAMESPACE" exec -i "${PG_STATEFULSET}-0" -- \
|
||||||
|
psql -U "$PG_USER" -d "$PG_DB" < "$SQL_FILE" > /dev/null
|
||||||
|
echo " Database restored."
|
||||||
|
|
||||||
|
# --- Step 4: Restore repositories ---
|
||||||
|
echo ""
|
||||||
|
echo "[4/5] Restoring repositories..."
|
||||||
|
|
||||||
|
# Need a helper pod on the same node as the PVC (RWO)
|
||||||
|
cleanup
|
||||||
|
kubectl -n "$NAMESPACE" run "$HELPER_POD" --restart=Never \
|
||||||
|
--image="$HELPER_IMAGE" \
|
||||||
|
--overrides="{
|
||||||
|
\"spec\":{
|
||||||
|
\"containers\":[{
|
||||||
|
\"name\":\"$HELPER_POD\",
|
||||||
|
\"image\":\"$HELPER_IMAGE\",
|
||||||
|
\"command\":[\"sleep\",\"3600\"],
|
||||||
|
\"volumeMounts\":[{\"name\":\"data\",\"mountPath\":\"/data\"}]
|
||||||
|
}],
|
||||||
|
\"volumes\":[{
|
||||||
|
\"name\":\"data\",
|
||||||
|
\"persistentVolumeClaim\":{\"claimName\":\"$PVC\"}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}" > /dev/null 2>&1
|
||||||
|
|
||||||
|
kubectl -n "$NAMESPACE" wait --for=condition=Ready "pod/$HELPER_POD" --timeout=120s > /dev/null 2>&1
|
||||||
|
|
||||||
|
# Clear existing repos
|
||||||
|
kubectl -n "$NAMESPACE" exec "$HELPER_POD" -- sh -c "rm -rf /data/gitea/repositories/*" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Upload repos — tar via stdin since kubectl cp needs tar in the container
|
||||||
|
echo " Uploading repositories..."
|
||||||
|
if [ "$REPOS_SOURCE" = "dir" ]; then
|
||||||
|
# repos/ directory — tar and stream into the PVC
|
||||||
|
tar -cf - -C "$REPOS_PATH" . | kubectl -n "$NAMESPACE" exec -i "$HELPER_POD" -- sh -c "tar -xf - -C /data/gitea/repositories/"
|
||||||
|
else
|
||||||
|
# gitea-repo.zip — stream and extract
|
||||||
|
cat "$REPOS_PATH" | kubectl -n "$NAMESPACE" exec -i "$HELPER_POD" -- sh -c "cat > /tmp/gitea-repo.zip && unzip -q -o /tmp/gitea-repo.zip -d /data/gitea/repositories/ && rm /tmp/gitea-repo.zip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restore data/ directory (avatars, attachments, LFS, etc.) if present
|
||||||
|
if [ -d "$RESTORE_DIR/data" ]; then
|
||||||
|
echo " Uploading data (avatars, attachments, LFS)..."
|
||||||
|
tar -cf - -C "$RESTORE_DIR/data" . | kubectl -n "$NAMESPACE" exec -i "$HELPER_POD" -- sh -c "tar -xf - -C /data/gitea/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fix ownership
|
||||||
|
kubectl -n "$NAMESPACE" exec "$HELPER_POD" -- chown -R 1000:1000 /data/gitea/
|
||||||
|
echo " Repositories restored."
|
||||||
|
|
||||||
|
# --- Step 5: Scale back up ---
|
||||||
|
echo ""
|
||||||
|
echo "[5/5] Scaling Gitea back up..."
|
||||||
|
kubectl -n "$NAMESPACE" scale deployment "$DEPLOYMENT" --replicas=1
|
||||||
|
kubectl -n "$NAMESPACE" rollout status deployment "$DEPLOYMENT" --timeout=120s
|
||||||
|
|
||||||
|
# Cleanup temp dir
|
||||||
|
rm -rf "$RESTORE_DIR"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Restore complete ==="
|
||||||
|
echo "Gitea should be back online with restored data."
|
||||||
|
echo "Verify at your Gitea URL that repos and users are present."
|
||||||
19
secrets/gitea-backup-s3-sealed.yaml
Normal file
19
secrets/gitea-backup-s3-sealed.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
apiVersion: bitnami.com/v1alpha1
|
||||||
|
kind: SealedSecret
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
name: gitea-backup-s3
|
||||||
|
namespace: gitea
|
||||||
|
spec:
|
||||||
|
encryptedData:
|
||||||
|
AWS_ACCESS_KEY_ID: AgByl2/ljFIHzTD0Vy7srQoXRfdJRLG+WukgeLMJeiJm9MOFJBNEkr5ju2DemDNdRcViXQLN3yxqT/L0fG0rz+kaPQtLeVFToqr58vokxDasHw4WVIUOosi6wE+yaI16H6vxvdV8dck09nHE3fdBcwctlvjqsY7mKvyx4tYKdGRDoeJ/C7shYoDTl4E/ZtsSRkfOQ4Ojm6M6FU10zn03OOKrzaOUczxnqbAyGNFrZvCUGG38QQVnyYm/2HLofQgheSSQx8p0w5IgPRRhBM3IAyCLGkyEA8qHXSO3J5Y2m1izAoU9RVsHAVUMVTfYEdtUMADkDcywKzi0bi0ehBxDs2PuC89Uz4s6rKQ/v8xU+jUf3KogrZxsbuCh1iFVO/NCOLvQhLY5wU/946wl1WmS76/HxkM/D9Iq+KF0VsP0pJAA+SIyJQ3Bh0a9GnKRsJjCfO8qX0M0WSXhOpjw+4DvBoe653mV7n+LOEjQy7LJURFaz1HzQColelhhlQ5tHCN8J1jtjscFNiHZqyzBBm226X3oxr0cAC6e/l53ohNkKS3NA5aM/wouBrscs1/CfmDYKxujLyGqontFRQc3rtCZ29829u/RmuwietIVGeu+ooCacSM73zDqGYKM7HRr/Y7QYxuW0TiSaJhYMZQqsC4uo7ebZhRa2bWbCTHiVCs3jDdpSRyPgEECOvnOJbkTsh0e02HtrUEx7HBjLZua9FD2sskr8C9XJQ==
|
||||||
|
AWS_SECRET_ACCESS_KEY: AgCYxgmto8ytLz8QMm25/nIqqezlWWennhbPSPMB/aDYR+zW45LvAZbjwVp6wwSR/U5iXwfdZg5/k8+8CzGAKDjxc3Nwygih3cUpqVBOl+uOzD3W3oNDsyQckhmNA4jidwIbJF6ecV8O+GVuU19+E4QrkHTIP9lN5pnhkfIR7nMRVj4jdcNahH2O75huadGQII4GG+rmnGX1012IAhknq20CiOCbby3a2yHaU3om0srO1TkW/67jioQX2IvgUh7jVl6c4r1Y6b+glwV4bHc9GecDqNEF6uj6uy8ChNh1khRfUYVysIQRM9m1pV/qlKiUW/wjDZOjoW88IAg4wl2MMOFQby27jVwQWSe+kUPwRMf7HSNWoq/DaE/z71cMsdeEnAXtQMGwNzOr4EGM1n/faPGDWkj306l1xjoXNO2hLCNX8BspSBxQDWWADBGClC6C1AQX0HlsZLV0G18VCEkjTwvPRmPqigxzHganxWiWM0q3DfGrc+JvnGFW0r7waoKI5vIzxwzCbb3I042+3z2vsvo7ZW2mez+eKgeD0MvhRW2SlBMiE62MGJQL2BvTew1iU0Xean+19WGZO7PPysnOH6kU098kTJ5GjQpxlI2C+w6QC18q8eQeIvVyd/7wH+k7RMCDC+No6MCcYhDlcQNbIir6JJ7vIOd3n5NKXdg7Sy3SnjkyDPTOjXTwyn2hHkATMzUxgn/0frNZYSsEMTuoNlOfcZLr7UbFv+Qlr49rkAEMo3deohfGiQSD
|
||||||
|
S3_BUCKET: AgA6ulIpP/DrYOQ7iqo7CSeaSj0L/+PXDPZ7SxPmdu/wrqbXw7nlxAyp+7QHqkUub5XgVhwrk3KPmqUPcECPDbHdt93+nlM3PVD0yPNkVaijncEPRVccGu/VhE6Nae5lXI9U3pnAVAXn+7z8iwRpF/vr1qIGyQwiizsKyfBQhvRSupzOvY8sypbItDjyjttxlwMRorGI94GObeUS89kSx/MB7BWZJMtUuRRSG6YwgH/XIkIWbo2p2LD90EhNtaT9rYa1PcGP9BVcgHf/9zVCI4+1LWbfmZSgobwAEKLhzZfhzCM2DhN31CsVWhpp0x0gYmspNbtQQvoosKmeBPBT+BLkTabAhjx00rvVX3J48Er3PaVAjw6JxT1KSdaUuCmcIzX3O8ys/8PNacaEgEiqmeuIPgID8YHSXSfs9RIUkjKBWGydjE90lMQPgnqOBkPWTd1BNRqHj60D2pFp0/h7+j8/OfBj7doDp9ECwcdQqjwzX4pNi7WQiGd+Ri0/7DK1xSAOL0lwgg8VSrqCOIdasAZRVQuHhuWwKMyhdQyQCr+zCOQ/bLQaPeF1m7tKxFfU4lNz5tRiC8AOQI5aHX2gRrkpfugD3G9qFFQMl9EPCdNeBh/ezVWxSxekWvQTuGJ2WnLD33BhsZVLKjXa+tHjD/BsQjQgdCiqv8J9gPgtngx/pAFf0NaQezVU4tBfaYD4tetcrZDz5UtW5tTHaF4=
|
||||||
|
S3_ENDPOINT: AgBfKbxdU9hZrxIpbM6b+hDthXQ+uYrjWHGmxSdjGvxgHB2P7E+HqblPAHjIpiAsGiPESV75ZKs5/BdEBOoZpbvneAuRgVLZ5mxkWiQ35q7sJpWaUg47icnlEPFoFj8oxYbi1NRAYB5hbc3AU0s11mw7wre0pRZu0pgSidHk/lyuSXOHKQzuhXxKYmV61LjMxCQGwDNwbiDNuSZyU7AZ2r+vr2W7Tzu6G+tJctwQd3HOnYbOLMV4tBv93nc7EkU4tbdvdIvkGHEmKf4r4F+nGvKZ3fZie1QKyQvG/4+i8OKqby9XJtviEEfBqfrk5qb1dNQlqCfA4ThQ11MmRiP8VoaUp/yoUHHYACNY9HLBp+N5Cgbbcxo044U1c8b97I6ZOZJ2waZ9XkrBpYPPXWJRKxLeNgYoJqn3yMZV/U561DO1jLZ2cwQXXaFrm1WT7VjcB0czdJHW3FcOg9lzYKMCCTTX+cD4M1oK992931eECQxBecrtlQYD+NlJng8ARm7myTACOZGYMQo2gjdM4ZBh9KqoCT2jrFC6E29YwfRAIXrhiWdZZxOW6Bu9Txt8FgxnIlSz9iZ1hvbfdvrSZTilJbAAULKFqLUgNpQbdgYHtGXQkzFHqYmbdZ0vJ6taIli7y+/Rz6xKcql8uJLxnuncLvLvXHxXl+rWeKrAMn+jPvnuCcdq6yVPsI0Nz/B4EQRL7Nzl9XYQxSybAJACrrCjgEuHsquoPpuznlGuk2scuakXdWOzMg6i/MEk
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
name: gitea-backup-s3
|
||||||
|
namespace: gitea
|
||||||
|
type: Opaque
|
||||||
Reference in New Issue
Block a user