diff --git a/README.md b/README.md index 0d48f54..befa9b2 100644 --- a/README.md +++ b/README.md @@ -334,6 +334,7 @@ kubectl patch application myapp -n argocd \ | **Loki** | Logs | `monitoring` | 1 | | **Tempo** | Distributed tracing | `monitoring` | 1 | | **Fluent-Bit** | Log shipping | `monitoring` | DaemonSet | +| **OpenCost** | Cost monitoring | `monitoring` | 1 | | **Trivy** | Vulnerability scanning | `trivy-system` | 1 | **Full specs**: [Technical Reference - Infrastructure Components](docs/REFERENCE.md#infrastructure-components) diff --git a/apps/argo-mcp.yaml b/apps/argo-mcp.yaml index ca12650..30b2874 100644 --- a/apps/argo-mcp.yaml +++ b/apps/argo-mcp.yaml @@ -16,14 +16,14 @@ metadata: spec: project: default sources: - - repoURL: git@github.com:fortedigital/forte-helm + - repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git path: forteapp targetRevision: HEAD helm: valueFiles: - $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 ref: values diff --git a/apps/mcp10x.yaml b/apps/mcp10x.yaml index e487f85..984c63a 100644 --- a/apps/mcp10x.yaml +++ b/apps/mcp10x.yaml @@ -17,14 +17,14 @@ metadata: spec: project: default sources: - - repoURL: git@github.com:fortedigital/forte-helm + - repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git path: forteapp targetRevision: HEAD helm: valueFiles: - $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 ref: values diff --git a/apps/musicman.yaml b/apps/musicman.yaml index da6377d..c29b9a2 100644 --- a/apps/musicman.yaml +++ b/apps/musicman.yaml @@ -17,14 +17,14 @@ metadata: spec: project: default sources: - - repoURL: git@github.com:fortedigital/forte-helm + - repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git path: forteapp targetRevision: HEAD helm: valueFiles: - $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 ref: values diff --git a/cluster-resources/gitea-backup-cronjob.yaml b/cluster-resources/gitea-backup-cronjob.yaml new file mode 100644 index 0000000..d05ec17 --- /dev/null +++ b/cluster-resources/gitea-backup-cronjob.yaml @@ -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 diff --git a/cluster-resources/gitea-ssh-ingressroute.yaml b/cluster-resources/gitea-ssh-ingressroute.yaml new file mode 100644 index 0000000..fb68b90 --- /dev/null +++ b/cluster-resources/gitea-ssh-ingressroute.yaml @@ -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 diff --git a/cluster-resources/policies/auth-sidecar-injector.yaml b/cluster-resources/policies/auth-sidecar-injector.yaml index fd2af7f..6d47dc8 100644 --- a/cluster-resources/policies/auth-sidecar-injector.yaml +++ b/cluster-resources/policies/auth-sidecar-injector.yaml @@ -127,7 +127,7 @@ spec: spec: containers: - 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: - containerPort: "{{ sidecarPort }}" name: auth @@ -208,7 +208,7 @@ spec: spec: containers: - 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 ports: - containerPort: "{{ sidecarPort }}" @@ -301,7 +301,7 @@ spec: spec: containers: - 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 ports: - containerPort: "{{ sidecarPort }}" @@ -380,7 +380,7 @@ spec: spec: containers: - 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 ports: - containerPort: "{{ sidecarPort }}" diff --git a/docs/OPERATIONS-RUNBOOK.md b/docs/OPERATIONS-RUNBOOK.md index aa29eff..e222165 100644 --- a/docs/OPERATIONS-RUNBOOK.md +++ b/docs/OPERATIONS-RUNBOOK.md @@ -180,7 +180,7 @@ Save the following file in private/ (gitignored) folder as secret.yaml argocd.argoproj.io/secret-type: repository stringData: type: git - url: git@github.com:fortedigital/forte-helm.git + url: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git sshPrivateKey: | project: default diff --git a/infra/gitea-actions.yaml b/infra/gitea-actions.yaml index 0f763cf..adb95e3 100644 --- a/infra/gitea-actions.yaml +++ b/infra/gitea-actions.yaml @@ -40,3 +40,9 @@ spec: - CreateNamespace=true - Validate=true - ServerSideApply=true + + ignoreDifferences: + - group: apps + kind: StatefulSet + jsonPointers: + - /spec/volumeClaimTemplates diff --git a/infra/gitea.yaml b/infra/gitea.yaml index 317193f..b219a72 100644 --- a/infra/gitea.yaml +++ b/infra/gitea.yaml @@ -40,3 +40,9 @@ spec: - CreateNamespace=true - Validate=true - ServerSideApply=true + + ignoreDifferences: + - group: apps + kind: StatefulSet + jsonPointers: + - /spec/volumeClaimTemplates diff --git a/infra/traefik-application.yaml b/infra/traefik-application.yaml index f239ce2..1fb13ab 100644 --- a/infra/traefik-application.yaml +++ b/infra/traefik-application.yaml @@ -76,6 +76,10 @@ spec: { "name": "websecure", "mode": "tcp" + }, + { + "name": "giteassh", + "mode": "tcp" } ], "backends": [ @@ -90,6 +94,9 @@ spec: "properties": { "outbound_proxy_protocol": "v2" } + }, + { + "name": "giteassh" } ] } @@ -129,6 +136,13 @@ spec: metrics: true tracing: true + giteassh: + port: 2222 + expose: + default: true + exposedPort: 2222 + protocol: TCP + destination: server: https://kubernetes.default.svc namespace: traefik-system diff --git a/infra/values/gitea-actions-values.yaml b/infra/values/gitea-actions-values.yaml index 288fda5..c659ec1 100644 --- a/infra/values/gitea-actions-values.yaml +++ b/infra/values/gitea-actions-values.yaml @@ -3,7 +3,7 @@ enabled: true -giteaRootURL: http://gitea-http.gitea.svc.cluster.local:3000 +giteaRootURL: https://git.forteapps.net existingSecret: gitea-runner-token existingSecretKey: token @@ -30,8 +30,7 @@ statefulset: docker_timeout: 300s runner: labels: - - "ubuntu-latest:docker://node:20-bookworm" - - "ubuntu-22.04:docker://node:20-bookworm" - + - "ubuntu-latest:docker://catthehacker/ubuntu:act-22.04" + - "ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04" dind: rootless: false diff --git a/infra/values/gitea-values.yaml b/infra/values/gitea-values.yaml index 58f3620..bf580bb 100644 --- a/infra/values/gitea-values.yaml +++ b/infra/values/gitea-values.yaml @@ -17,13 +17,17 @@ gitea: DOMAIN: git.forteapps.net ROOT_URL: https://git.forteapps.net SSH_DOMAIN: git.forteapps.net - SSH_PORT: 22 + SSH_PORT: 2222 LFS_START_SERVER: true + ENABLE_GITEA_PAGES: true service: DISABLE_REGISTRATION: false + DEFAULT_ALLOW_CREATE_ORGANIZATION: false REQUIRE_SIGNIN_VIEW: false ALLOW_ONLY_EXTERNAL_REGISTRATION: true + ENABLE_BASIC_AUTHENTICATION: true + ENABLE_PASSWORD_SIGNIN_FORM: false openid: ENABLE_OPENID_SIGNIN: false @@ -67,8 +71,8 @@ gitea: existingSecret: gitea-credentials key: gitea autoDiscoverUrl: "https://id.forteapps.net/realms/forte/.well-known/openid-configuration" - scopes: "openid email profile" - groupClaimName: "" + scopes: "openid email profile organization" + groupClaimName: "groups" adminGroup: "" restrictedGroup: "" # -- Prometheus metrics (scraped via annotations, no ServiceMonitor CRD needed) @@ -146,7 +150,7 @@ redis-cluster: test: 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: ssh: type: ClusterIP diff --git a/infra/values/opencost-values.yaml b/infra/values/opencost-values.yaml index cee1465..39d73cc 100644 --- a/infra/values/opencost-values.yaml +++ b/infra/values/opencost-values.yaml @@ -15,10 +15,10 @@ opencost: provider: custom costModel: description: "UpCloud 4-node cluster pricing" - CPU: "6.07" - RAM: "1.52" + CPU: "5.86" + RAM: "1.46" GPU: "0" - storage: "0.03" + storage: "0.34" zoneNetworkEgress: "0" regionNetworkEgress: "0" internetNetworkEgress: "0" diff --git a/scripts/gitea-backup.sh b/scripts/gitea-backup.sh new file mode 100644 index 0000000..397a6fa --- /dev/null +++ b/scripts/gitea-backup.sh @@ -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 # 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 }" + + 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 Download a specific backup" + echo " $0 download latest Download the most recent backup" + ;; +esac diff --git a/scripts/gitea-restore.sh b/scripts/gitea-restore.sh new file mode 100644 index 0000000..34c9ac7 --- /dev/null +++ b/scripts/gitea-restore.sh @@ -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 +# +# 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 }" + +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." diff --git a/secrets/gitea-backup-s3-sealed.yaml b/secrets/gitea-backup-s3-sealed.yaml new file mode 100644 index 0000000..0e185e3 --- /dev/null +++ b/secrets/gitea-backup-s3-sealed.yaml @@ -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