Add migration scripts and manifests for GitLab and Gitea to Longhorn
- Create .vscode/settings.json for YAML schema validation. - Add WISSENSBASIS.md for documentation on HomeLabScripts. - Implement migration job for GitLab from NFS to Longhorn with migrate-to-longhorn.yaml and migrate-to-longhorn.sh. - Add Gitea migration scripts and manifests for PostgreSQL to Longhorn. - Create persistent volume claims and deployments for Gitea and Homarr. - Set up namespaces and services for Homarr and Speedtest Tracker. - Add secrets for Homarr and Speedtest Tracker with sensitive data. - Configure Ingress for Speedtest Tracker with Traefik annotations. Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"yaml.schemas": {
|
||||||
|
"kubernetes://schema/v1@persistentvolumeclaim": "file:///home/henry/HomeLabScripts/k3s/apps/gitLab/manifest/pv-pvc.yaml"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# HomeLabScripts – Wissensbasis
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
Persönliches Home-Lab-Infrastruktur-Repository. Orchestriert eine selbst gehostete Cloud-Umgebung auf K3s (Kubernetes) auf lokaler Hardware.
|
||||||
|
|
||||||
|
## Technologien
|
||||||
|
- **K3s** – Kubernetes-Distribution
|
||||||
|
- **Helm** – Paketmanager für K8s
|
||||||
|
- **NFS** – Netzwerk-Dateisystem (shared storage)
|
||||||
|
- **Longhorn** – verteilter Block-Storage (2 Replicas)
|
||||||
|
- **PostgreSQL / MariaDB / Redis** – Datenbanken & Caching
|
||||||
|
- **Authentik** – OAuth2/OIDC-Authentifizierungsserver
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repo-Struktur
|
||||||
|
```
|
||||||
|
HomeLabScripts/
|
||||||
|
├── k3s/
|
||||||
|
│ ├── apps/ # App-Manifeste & Helm-Charts
|
||||||
|
│ ├── install.sh # K3s-Cluster-Installation
|
||||||
|
│ ├── get_helm.sh # Helm herunterladen
|
||||||
|
│ ├── installHelm.sh # Helm installieren
|
||||||
|
│ └── k8sUser/ # Benutzer-/Kubeconfig-Setup
|
||||||
|
├── nfs/ # NFS-Server- & Client-Skripte
|
||||||
|
└── mountscript/ # Disk-Partitionierung & Einhängen
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anwendungen
|
||||||
|
|
||||||
|
| App | Namespace | NodePort | Storage | Beschreibung |
|
||||||
|
|-----|-----------|----------|---------|--------------|
|
||||||
|
| **Authentik** | authentik | 32222 | PostgreSQL (intern) | OAuth2/OIDC-Provider |
|
||||||
|
| **Homarr** | homarr | 30757 | Longhorn 5Gi | Homepage-Dashboard |
|
||||||
|
| **K8s Dashboard** | kubernetes-dashboard | 443 | – | Cluster-Management-UI |
|
||||||
|
| **Gitea** | gitea | – | NFS 30Gi (Repos) + 10Gi (DB) | Leichter Git-Dienst |
|
||||||
|
| **GitLab** | gitlab | 80/443/22 | NFS 50Gi (RWX) | Full GitLab mit CI/CD |
|
||||||
|
| **Nextcloud** | nextcloud | 30180 | NFS (Daten) + Longhorn (DB) | Datei-Hosting |
|
||||||
|
| **Immich** | photoprism | 3001/2283 | NFS photos | Fotoverwaltung (Google Photos Alternative) |
|
||||||
|
| **PhotoPrism** | photoprism | – | NFS photos | KI-Fotoverwaltung |
|
||||||
|
| **iCloudPD** | photoprism | – | NFS /data/originals | Apple-iCloud-Foto-Sync |
|
||||||
|
| **Longhorn** | longhorn-system | – | – | Storage-Provisioner |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Storage-Strategie
|
||||||
|
- **Longhorn** → Datenbanken, kleine Konfigurationen (schnell, lokal)
|
||||||
|
- **NFS** → Medien, Repos, Nextcloud-Daten (groß, geteilt, RWX)
|
||||||
|
- NFS-Server: `192.168.178.166`, Pfade: `/export/fastData/`, `/export/slowData/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Netzwerk
|
||||||
|
- **NodePort** für externen Zugriff
|
||||||
|
- **ClusterIP** für Pod-to-Pod-Kommunikation (DBs)
|
||||||
|
- **Multus** bei Immich (separates IoT-Netz: `192.168.1.192/24`)
|
||||||
|
- Domain: `henryathome.home64.de`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Datenbank-Zuordnung
|
||||||
|
| App | DB-Typ | User | Hinweis |
|
||||||
|
|-----|--------|------|---------|
|
||||||
|
| Authentik | PostgreSQL | – | intern |
|
||||||
|
| Nextcloud | MariaDB 10.8 | nextcloud | PW: nextcloud |
|
||||||
|
| GitLab | MariaDB | – | NFS-Backend |
|
||||||
|
| Gitea | PostgreSQL | – | NFS-Backend |
|
||||||
|
| Immich | PostgreSQL 14 (pgvecto-rs) | immich | PW: password |
|
||||||
|
| PhotoPrism | MariaDB | photoprism | PW: photoprism |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wichtige Skripte
|
||||||
|
|
||||||
|
| Skript | Zweck |
|
||||||
|
|--------|-------|
|
||||||
|
| `k3s/install.sh` | K3s installieren |
|
||||||
|
| `k3s/installHelm.sh` | Helm 3 installieren |
|
||||||
|
| `k3s/k8sUser/addUser.sh` | ServiceAccount + ClusterRoleBinding + Kubeconfig erstellen |
|
||||||
|
| `k3s/apps/dashboard/getToken.sh` | Admin-Token für K8s Dashboard |
|
||||||
|
| `k3s/apps/photo/icloudpd/base64pw.sh` | iCloud-Passwort base64-kodieren |
|
||||||
|
| `k3s/apps/Nextcloud/helm/cleanRestart.sh` | Nextcloud sauber neu starten |
|
||||||
|
| `nfs/server.sh` | NFS-Server konfigurieren |
|
||||||
|
| `nfs/client.sh` / `nfsClient2.sh` | NFS-Client einrichten & in fstab eintragen |
|
||||||
|
| `mountscript/mount-plus.sh` | Festplatte partitionieren, formatieren, einhängen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Muster & Konventionen
|
||||||
|
- Secrets: `*-secret.yaml` je App, base64-kodiert
|
||||||
|
- Init-Container: Warten auf DB-Bereitschaft (Nextcloud, Immich)
|
||||||
|
- `imagePullPolicy: IfNotPresent` (kein automatisches Re-Pull)
|
||||||
|
- `nodeSelector: knode0` bei Nextcloud (Kernel 6.1 für NFS)
|
||||||
|
- fsGroup für NFS-Berechtigungen (z. B. `33` für www-data)
|
||||||
|
- GitLab-Runner-Token: `glrt-3nNma_nEvL1Bq2zc8m5Zu286MQpwOjIKdDozCnU6MTAQ`
|
||||||
@@ -99,7 +99,7 @@ spec:
|
|||||||
volumes:
|
volumes:
|
||||||
- name: gitlab-data # lokal (postgresql, redis, etc.)
|
- name: gitlab-data # lokal (postgresql, redis, etc.)
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: gitlab-data-pvc
|
claimName: gitlab-data-longhorn-pvc
|
||||||
|
|
||||||
- name: gitlab-git # NFS (Git-Repositories)
|
- name: gitlab-git # NFS (Git-Repositories)
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
@@ -107,7 +107,7 @@ spec:
|
|||||||
|
|
||||||
- name: gitlab-config # lokal
|
- name: gitlab-config # lokal
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: gitlab-config-pvc
|
claimName: gitlab-config-longhorn-pvc
|
||||||
|
|
||||||
- name: gitlab-logs # ephemeral
|
- name: gitlab-logs # ephemeral
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: gitlab-migrate-to-longhorn
|
||||||
|
namespace: gitlab
|
||||||
|
spec:
|
||||||
|
backoffLimit: 4
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
name: gitlab-migrate-to-longhorn
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
containers:
|
||||||
|
- name: migrate
|
||||||
|
image: alpine:3.18
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -euo pipefail
|
||||||
|
apk add --no-cache rsync
|
||||||
|
echo "Starting migration: /var/opt/gitlab (data)"
|
||||||
|
rsync -aHAX --numeric-ids --delete /var/opt_gitlab_old/ /var/opt_gitlab_new/
|
||||||
|
echo "Finished data copy. Starting config copy: /etc/gitlab"
|
||||||
|
rsync -aHAX --numeric-ids --delete /etc_gitlab_old/ /etc_gitlab_new/
|
||||||
|
echo "Migration complete"
|
||||||
|
volumeMounts:
|
||||||
|
- name: old-data
|
||||||
|
mountPath: /var/opt_gitlab_old
|
||||||
|
- name: new-data
|
||||||
|
mountPath: /var/opt_gitlab_new
|
||||||
|
- name: old-config
|
||||||
|
mountPath: /etc_gitlab_old
|
||||||
|
- name: new-config
|
||||||
|
mountPath: /etc_gitlab_new
|
||||||
|
volumes:
|
||||||
|
- name: old-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: gitlab-data-pvc
|
||||||
|
- name: new-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: gitlab-data-longhorn-pvc
|
||||||
|
- name: old-config
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: gitlab-config-pvc
|
||||||
|
- name: new-config
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: gitlab-config-longhorn-pvc
|
||||||
@@ -96,3 +96,32 @@ spec:
|
|||||||
requests:
|
requests:
|
||||||
storage: 1Gi
|
storage: 1Gi
|
||||||
volumeName: gitlab-config-pv
|
volumeName: gitlab-config-pv
|
||||||
|
|
||||||
|
---
|
||||||
|
# ─── Neue Longhorn-PVCs zum Migrieren der Daten (dynamisch provisioniert) ─
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: gitlab-data-longhorn-pvc
|
||||||
|
namespace: gitlab
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 20Gi
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: gitlab-config-longhorn-pvc
|
||||||
|
namespace: gitlab
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
|
|||||||
Executable
+254
@@ -0,0 +1,254 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
NAMESPACE="gitlab"
|
||||||
|
DEPLOYMENT_NAME="gitlab"
|
||||||
|
JOB_NAME="gitlab-migrate-to-longhorn"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
MANIFEST_DIR="${SCRIPT_DIR}/manifest"
|
||||||
|
PV_PVC_MANIFEST="${MANIFEST_DIR}/pv-pvc.yaml"
|
||||||
|
MIGRATION_JOB_MANIFEST="${MANIFEST_DIR}/migrate-to-longhorn.yaml"
|
||||||
|
|
||||||
|
SOURCE_DATA_PVC="gitlab-data-pvc"
|
||||||
|
SOURCE_CONFIG_PVC="gitlab-config-pvc"
|
||||||
|
TARGET_DATA_PVC="gitlab-data-longhorn-pvc"
|
||||||
|
TARGET_CONFIG_PVC="gitlab-config-longhorn-pvc"
|
||||||
|
|
||||||
|
BIND_TIMEOUT_SECONDS=900
|
||||||
|
JOB_TIMEOUT_SECONDS=7200
|
||||||
|
POD_STOP_TIMEOUT_SECONDS=300
|
||||||
|
|
||||||
|
ORIGINAL_REPLICAS="1"
|
||||||
|
SCALED_DOWN="false"
|
||||||
|
DRY_RUN="false"
|
||||||
|
AUTO_CONFIRM="false"
|
||||||
|
|
||||||
|
info() {
|
||||||
|
echo "[INFO] $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo "[WARN] $*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
err() {
|
||||||
|
echo "[ERROR] $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: migrate-to-longhorn.sh [--dry-run] [--yes] [--help]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--dry-run Show planned actions without changing cluster state.
|
||||||
|
--yes Skip interactive confirmation before scaling down GitLab.
|
||||||
|
--help Show this help.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run)
|
||||||
|
DRY_RUN="true"
|
||||||
|
;;
|
||||||
|
--yes)
|
||||||
|
AUTO_CONFIRM="true"
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
err "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_deployment_on_error() {
|
||||||
|
if [[ "${SCALED_DOWN}" == "true" ]]; then
|
||||||
|
warn "Migration failed. Restoring deployment replicas to ${ORIGINAL_REPLICAS}."
|
||||||
|
kubectl -n "${NAMESPACE}" scale deployment "${DEPLOYMENT_NAME}" --replicas="${ORIGINAL_REPLICAS}" >/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
on_exit() {
|
||||||
|
local exit_code=$?
|
||||||
|
if [[ ${exit_code} -ne 0 ]]; then
|
||||||
|
restore_deployment_on_error
|
||||||
|
warn "Script failed with exit code ${exit_code}."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap on_exit EXIT
|
||||||
|
|
||||||
|
require_tool() {
|
||||||
|
local tool="$1"
|
||||||
|
command -v "${tool}" >/dev/null 2>&1 || err "Required tool not found: ${tool}"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_file() {
|
||||||
|
local path="$1"
|
||||||
|
[[ -f "${path}" ]] || err "Required file not found: ${path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_pvc_bound() {
|
||||||
|
local pvc_name="$1"
|
||||||
|
local timeout="$2"
|
||||||
|
local elapsed=0
|
||||||
|
local interval=5
|
||||||
|
|
||||||
|
info "Waiting for PVC ${pvc_name} to become Bound..."
|
||||||
|
while true; do
|
||||||
|
local phase
|
||||||
|
phase="$(kubectl -n "${NAMESPACE}" get pvc "${pvc_name}" -o jsonpath='{.status.phase}' 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [[ "${phase}" == "Bound" ]]; then
|
||||||
|
info "PVC ${pvc_name} is Bound."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( elapsed >= timeout )); then
|
||||||
|
err "Timeout while waiting for PVC ${pvc_name} to bind."
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "${interval}"
|
||||||
|
elapsed=$((elapsed + interval))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_gitlab_pods_stopped() {
|
||||||
|
local timeout="$1"
|
||||||
|
local elapsed=0
|
||||||
|
local interval=5
|
||||||
|
|
||||||
|
info "Waiting for pods with label app=gitlab to stop..."
|
||||||
|
while true; do
|
||||||
|
local count
|
||||||
|
count="$(kubectl -n "${NAMESPACE}" get pods -l app=gitlab --no-headers 2>/dev/null | wc -l | tr -d ' ')"
|
||||||
|
|
||||||
|
if [[ "${count}" == "0" ]]; then
|
||||||
|
info "All GitLab pods are stopped."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( elapsed >= timeout )); then
|
||||||
|
err "Timeout while waiting for GitLab pods to stop."
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "${interval}"
|
||||||
|
elapsed=$((elapsed + interval))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_job_complete() {
|
||||||
|
local timeout="$1"
|
||||||
|
|
||||||
|
info "Waiting for migration job ${JOB_NAME} to complete..."
|
||||||
|
if kubectl -n "${NAMESPACE}" wait --for=condition=complete "job/${JOB_NAME}" --timeout="${timeout}s" >/dev/null; then
|
||||||
|
info "Migration job completed successfully."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn "Migration job did not complete in time or failed."
|
||||||
|
kubectl -n "${NAMESPACE}" describe job "${JOB_NAME}" || true
|
||||||
|
kubectl -n "${NAMESPACE}" logs "job/${JOB_NAME}" --tail=-1 || true
|
||||||
|
err "Migration job failed."
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_prerequisites() {
|
||||||
|
require_tool kubectl
|
||||||
|
require_file "${PV_PVC_MANIFEST}"
|
||||||
|
require_file "${MIGRATION_JOB_MANIFEST}"
|
||||||
|
|
||||||
|
kubectl get namespace "${NAMESPACE}" >/dev/null 2>&1 || err "Namespace does not exist: ${NAMESPACE}"
|
||||||
|
kubectl -n "${NAMESPACE}" get deployment "${DEPLOYMENT_NAME}" >/dev/null 2>&1 || err "Deployment not found: ${DEPLOYMENT_NAME}"
|
||||||
|
|
||||||
|
kubectl -n "${NAMESPACE}" get pvc "${SOURCE_DATA_PVC}" >/dev/null 2>&1 || err "Source PVC not found: ${SOURCE_DATA_PVC}"
|
||||||
|
kubectl -n "${NAMESPACE}" get pvc "${SOURCE_CONFIG_PVC}" >/dev/null 2>&1 || err "Source PVC not found: ${SOURCE_CONFIG_PVC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm_scale_down() {
|
||||||
|
if [[ "${AUTO_CONFIRM}" == "true" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
warn "GitLab deployment ${DEPLOYMENT_NAME} in namespace ${NAMESPACE} will be scaled to 0 during migration."
|
||||||
|
read -r -p "Type MIGRATE to continue: " confirmation
|
||||||
|
if [[ "${confirmation}" != "MIGRATE" ]]; then
|
||||||
|
err "Confirmation failed. Aborted by user."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_dry_run_plan() {
|
||||||
|
info "Dry-run mode active. No cluster changes will be made."
|
||||||
|
info "Planned steps:"
|
||||||
|
info "1) kubectl apply -f ${PV_PVC_MANIFEST}"
|
||||||
|
info "2) Wait for PVCs ${TARGET_DATA_PVC} and ${TARGET_CONFIG_PVC} to be Bound"
|
||||||
|
info "3) Scale deployment ${DEPLOYMENT_NAME} to 0"
|
||||||
|
info "4) Recreate and run job ${JOB_NAME} from ${MIGRATION_JOB_MANIFEST}"
|
||||||
|
info "5) Wait for job completion and verify target PVCs"
|
||||||
|
info "6) Scale deployment ${DEPLOYMENT_NAME} back to ${ORIGINAL_REPLICAS}"
|
||||||
|
info "No PVC switch in deployment is performed by this script."
|
||||||
|
}
|
||||||
|
|
||||||
|
run_migration() {
|
||||||
|
ORIGINAL_REPLICAS="$(kubectl -n "${NAMESPACE}" get deployment "${DEPLOYMENT_NAME}" -o jsonpath='{.spec.replicas}')"
|
||||||
|
ORIGINAL_REPLICAS="${ORIGINAL_REPLICAS:-1}"
|
||||||
|
|
||||||
|
if [[ "${DRY_RUN}" == "true" ]]; then
|
||||||
|
print_dry_run_plan
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
confirm_scale_down
|
||||||
|
|
||||||
|
info "Applying PVC manifest to ensure Longhorn target PVCs exist."
|
||||||
|
kubectl apply -f "${PV_PVC_MANIFEST}" >/dev/null
|
||||||
|
|
||||||
|
wait_for_pvc_bound "${TARGET_DATA_PVC}" "${BIND_TIMEOUT_SECONDS}"
|
||||||
|
wait_for_pvc_bound "${TARGET_CONFIG_PVC}" "${BIND_TIMEOUT_SECONDS}"
|
||||||
|
|
||||||
|
info "Scaling deployment ${DEPLOYMENT_NAME} down to 0 for consistent copy."
|
||||||
|
kubectl -n "${NAMESPACE}" scale deployment "${DEPLOYMENT_NAME}" --replicas=0 >/dev/null
|
||||||
|
SCALED_DOWN="true"
|
||||||
|
wait_for_gitlab_pods_stopped "${POD_STOP_TIMEOUT_SECONDS}"
|
||||||
|
|
||||||
|
info "Recreating migration job ${JOB_NAME}."
|
||||||
|
kubectl -n "${NAMESPACE}" delete job "${JOB_NAME}" --ignore-not-found >/dev/null
|
||||||
|
kubectl apply -f "${MIGRATION_JOB_MANIFEST}" >/dev/null
|
||||||
|
|
||||||
|
info "Streaming migration job logs."
|
||||||
|
kubectl -n "${NAMESPACE}" logs -f "job/${JOB_NAME}" || true
|
||||||
|
|
||||||
|
wait_for_job_complete "${JOB_TIMEOUT_SECONDS}"
|
||||||
|
|
||||||
|
info "Quick verification: target PVCs are still Bound."
|
||||||
|
wait_for_pvc_bound "${TARGET_DATA_PVC}" 30
|
||||||
|
wait_for_pvc_bound "${TARGET_CONFIG_PVC}" 30
|
||||||
|
|
||||||
|
info "Scaling deployment ${DEPLOYMENT_NAME} back to ${ORIGINAL_REPLICAS}."
|
||||||
|
kubectl -n "${NAMESPACE}" scale deployment "${DEPLOYMENT_NAME}" --replicas="${ORIGINAL_REPLICAS}" >/dev/null
|
||||||
|
SCALED_DOWN="false"
|
||||||
|
|
||||||
|
info "Migration finished successfully."
|
||||||
|
info "No deployment PVC switch was done by this script."
|
||||||
|
info "NFS PVC/PV (gitlab-git-pvc / gitlab-git-pv) were not changed."
|
||||||
|
info "If you want to switch GitLab to Longhorn later, apply the updated deployment manifest manually."
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
parse_args "$@"
|
||||||
|
info "Starting GitLab local PVC to Longhorn migration."
|
||||||
|
if [[ "${DRY_RUN}" == "true" ]]; then
|
||||||
|
info "Running in dry-run mode."
|
||||||
|
fi
|
||||||
|
validate_prerequisites
|
||||||
|
run_migration
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: gitea-runner-data
|
||||||
|
namespace: gitea
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: gitea-runner
|
||||||
|
namespace: gitea
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: gitea-runner
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: gitea-runner
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: runner
|
||||||
|
image: gitea/act_runner:latest
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
env:
|
||||||
|
- name: GITEA_INSTANCE_URL
|
||||||
|
value: "http://gitea.gitea.svc.cluster.local"
|
||||||
|
- name: GITEA_RUNNER_NAME
|
||||||
|
value: "k3s-runner-1"
|
||||||
|
- name: GITEA_RUNNER_LABELS
|
||||||
|
value: "linux-x64:host,ubuntu-latest:docker://node:20-bookworm,alpine:docker://alpine:3.20"
|
||||||
|
- name: GITEA_RUNNER_REGISTRATION_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: gitea-runner-secret
|
||||||
|
key: GITEA_RUNNER_REGISTRATION_TOKEN
|
||||||
|
- name: DOCKER_HOST
|
||||||
|
value: "tcp://localhost:2375"
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
args:
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
if [ ! -f /data/.runner ]; then
|
||||||
|
act_runner register \
|
||||||
|
--no-interactive \
|
||||||
|
--instance "${GITEA_INSTANCE_URL}" \
|
||||||
|
--token "${GITEA_RUNNER_REGISTRATION_TOKEN}" \
|
||||||
|
--name "${GITEA_RUNNER_NAME}" \
|
||||||
|
--labels "${GITEA_RUNNER_LABELS}"
|
||||||
|
fi
|
||||||
|
exec act_runner daemon
|
||||||
|
volumeMounts:
|
||||||
|
- name: runner-data
|
||||||
|
mountPath: /data
|
||||||
|
- name: dind
|
||||||
|
image: docker:27-dind
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
env:
|
||||||
|
- name: DOCKER_TLS_CERTDIR
|
||||||
|
value: ""
|
||||||
|
volumeMounts:
|
||||||
|
- name: docker-lib
|
||||||
|
mountPath: /var/lib/docker
|
||||||
|
volumes:
|
||||||
|
- name: runner-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: gitea-runner-data
|
||||||
|
- name: docker-lib
|
||||||
|
emptyDir: {}
|
||||||
@@ -65,6 +65,21 @@ spec:
|
|||||||
requests:
|
requests:
|
||||||
storage: 10Gi
|
storage: 10Gi
|
||||||
|
|
||||||
|
# PVC: PostgreSQL (Longhorn target)
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: postgres-longhorn-pvc
|
||||||
|
namespace: gitea
|
||||||
|
spec:
|
||||||
|
storageClassName: longhorn
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
|
|
||||||
# Deployment: PostgreSQL
|
# Deployment: PostgreSQL
|
||||||
---
|
---
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
@@ -74,6 +89,8 @@ metadata:
|
|||||||
namespace: gitea
|
namespace: gitea
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: postgres
|
app: postgres
|
||||||
@@ -98,13 +115,11 @@ spec:
|
|||||||
- name: postgres-storage
|
- name: postgres-storage
|
||||||
mountPath: /var/lib/postgresql/data
|
mountPath: /var/lib/postgresql/data
|
||||||
securityContext:
|
securityContext:
|
||||||
runAsUser: 1001
|
fsGroup: 999
|
||||||
runAsGroup: 1000
|
|
||||||
# fsGroup: 1000
|
|
||||||
volumes:
|
volumes:
|
||||||
- name: postgres-storage
|
- name: postgres-storage
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: postgres-pvc
|
claimName: postgres-longhorn-pvc
|
||||||
|
|
||||||
# Service: PostgreSQL
|
# Service: PostgreSQL
|
||||||
---
|
---
|
||||||
@@ -173,9 +188,7 @@ spec:
|
|||||||
- name: gitea-storage
|
- name: gitea-storage
|
||||||
mountPath: /data
|
mountPath: /data
|
||||||
securityContext:
|
securityContext:
|
||||||
# runAsUser: 1001
|
fsGroup: 1000
|
||||||
# runAsGroup: 1000
|
|
||||||
# fsGroup: 1000
|
|
||||||
volumes:
|
volumes:
|
||||||
- name: gitea-storage
|
- name: gitea-storage
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
|
|||||||
@@ -0,0 +1,335 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
NAMESPACE="gitea"
|
||||||
|
GITEA_DEPLOYMENT="gitea"
|
||||||
|
POSTGRES_DEPLOYMENT="postgres"
|
||||||
|
POSTGRES_VOLUME_NAME="postgres-storage"
|
||||||
|
JOB_NAME="gitea-postgres-migrate-to-longhorn"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TARGET_PVC_MANIFEST="${SCRIPT_DIR}/postgres-longhorn-pvc.yaml"
|
||||||
|
JOB_MANIFEST="${SCRIPT_DIR}/migrate-postgres-to-longhorn.yaml"
|
||||||
|
|
||||||
|
SOURCE_PVC="postgres-pvc"
|
||||||
|
TARGET_PVC="postgres-longhorn-pvc"
|
||||||
|
|
||||||
|
PVC_BIND_TIMEOUT_SECONDS=900
|
||||||
|
POD_STOP_TIMEOUT_SECONDS=300
|
||||||
|
POSTGRES_READY_TIMEOUT_SECONDS=300
|
||||||
|
JOB_TIMEOUT_SECONDS=3600
|
||||||
|
|
||||||
|
ORIGINAL_GITEA_REPLICAS="1"
|
||||||
|
ORIGINAL_POSTGRES_REPLICAS="1"
|
||||||
|
SOURCE_NODE_NAME=""
|
||||||
|
SCALED_DOWN_GITEA="false"
|
||||||
|
SCALED_DOWN_POSTGRES="false"
|
||||||
|
POSTGRES_CLAIM_SWITCHED="false"
|
||||||
|
DRY_RUN="false"
|
||||||
|
AUTO_CONFIRM="false"
|
||||||
|
|
||||||
|
info() {
|
||||||
|
echo "[INFO] $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo "[WARN] $*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
err() {
|
||||||
|
echo "[ERROR] $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: migrate-postgres-to-longhorn.sh [--dry-run] [--yes] [--help]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--dry-run Show planned actions without changing cluster state.
|
||||||
|
--yes Skip interactive confirmation before scaling down Gitea.
|
||||||
|
--help Show this help.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run)
|
||||||
|
DRY_RUN="true"
|
||||||
|
;;
|
||||||
|
--yes)
|
||||||
|
AUTO_CONFIRM="true"
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
err "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
require_tool() {
|
||||||
|
local tool="$1"
|
||||||
|
command -v "${tool}" >/dev/null 2>&1 || err "Required tool not found: ${tool}"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_file() {
|
||||||
|
local path="$1"
|
||||||
|
[[ -f "${path}" ]] || err "Required file not found: ${path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_pvc_bound() {
|
||||||
|
local pvc_name="$1"
|
||||||
|
local timeout="$2"
|
||||||
|
local elapsed=0
|
||||||
|
local interval=5
|
||||||
|
|
||||||
|
info "Waiting for PVC ${pvc_name} to become Bound..."
|
||||||
|
while true; do
|
||||||
|
local phase
|
||||||
|
phase="$(kubectl -n "${NAMESPACE}" get pvc "${pvc_name}" -o jsonpath='{.status.phase}' 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [[ "${phase}" == "Bound" ]]; then
|
||||||
|
info "PVC ${pvc_name} is Bound."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( elapsed >= timeout )); then
|
||||||
|
err "Timeout while waiting for PVC ${pvc_name} to bind."
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "${interval}"
|
||||||
|
elapsed=$((elapsed + interval))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_no_pods() {
|
||||||
|
local selector="$1"
|
||||||
|
local timeout="$2"
|
||||||
|
local elapsed=0
|
||||||
|
local interval=5
|
||||||
|
|
||||||
|
info "Waiting for pods with selector ${selector} to stop..."
|
||||||
|
while true; do
|
||||||
|
local count
|
||||||
|
count="$(kubectl -n "${NAMESPACE}" get pods -l "${selector}" --no-headers 2>/dev/null | awk '$3 != "Completed" && $3 != "Succeeded" {count++} END {print count+0}')"
|
||||||
|
|
||||||
|
if [[ "${count}" == "0" ]]; then
|
||||||
|
info "No pods left for selector ${selector}."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( elapsed >= timeout )); then
|
||||||
|
err "Timeout while waiting for pods with selector ${selector} to stop."
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "${interval}"
|
||||||
|
elapsed=$((elapsed + interval))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
discover_source_node() {
|
||||||
|
SOURCE_NODE_NAME="$(kubectl -n "${NAMESPACE}" get pod -l app=postgres -o jsonpath='{.items[0].spec.nodeName}' 2>/dev/null || true)"
|
||||||
|
[[ -n "${SOURCE_NODE_NAME}" ]] || err "Could not determine the current PostgreSQL node before shutdown."
|
||||||
|
info "Using source node ${SOURCE_NODE_NAME} for the migration job."
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_migration_job() {
|
||||||
|
sed "/restartPolicy: Never/a\ nodeName: ${SOURCE_NODE_NAME}" "${JOB_MANIFEST}" | kubectl apply -f - >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_deployment_ready() {
|
||||||
|
local deployment_name="$1"
|
||||||
|
local timeout="$2"
|
||||||
|
|
||||||
|
info "Waiting for deployment ${deployment_name} rollout to finish..."
|
||||||
|
kubectl -n "${NAMESPACE}" rollout status "deployment/${deployment_name}" --timeout="${timeout}s" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_job_complete() {
|
||||||
|
local timeout="$1"
|
||||||
|
|
||||||
|
info "Waiting for migration job ${JOB_NAME} to complete..."
|
||||||
|
if kubectl -n "${NAMESPACE}" wait --for=condition=complete "job/${JOB_NAME}" --timeout="${timeout}s" >/dev/null; then
|
||||||
|
info "Migration job completed successfully."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn "Migration job did not complete in time or failed."
|
||||||
|
kubectl -n "${NAMESPACE}" describe job "${JOB_NAME}" || true
|
||||||
|
kubectl -n "${NAMESPACE}" logs "job/${JOB_NAME}" --tail=-1 || true
|
||||||
|
err "Migration job failed."
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_target_data() {
|
||||||
|
local verification
|
||||||
|
verification="$(kubectl -n "${NAMESPACE}" get job "${JOB_NAME}" -o jsonpath='{.status.succeeded}' 2>/dev/null || true)"
|
||||||
|
[[ "${verification}" == "1" ]] || err "Migration job did not report success."
|
||||||
|
|
||||||
|
info "Verifying target PVC content markers before starting Gitea."
|
||||||
|
kubectl -n "${NAMESPACE}" run gitea-postgres-verify \
|
||||||
|
--rm --restart=Never --image=alpine:3.20 \
|
||||||
|
--overrides='{
|
||||||
|
"apiVersion":"v1",
|
||||||
|
"spec":{
|
||||||
|
"containers":[{
|
||||||
|
"name":"verify",
|
||||||
|
"image":"alpine:3.20",
|
||||||
|
"command":["sh","-c","set -e; test -f /target/PG_VERSION; test -d /target/base; find /target -mindepth 1 | wc -l; ls -la /target | head -n 20"],
|
||||||
|
"volumeMounts":[{
|
||||||
|
"name":"target",
|
||||||
|
"mountPath":"/target"
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
"volumes":[{
|
||||||
|
"name":"target",
|
||||||
|
"persistentVolumeClaim":{
|
||||||
|
"claimName":"postgres-longhorn-pvc"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}' \
|
||||||
|
--attach=true >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
patch_postgres_claim() {
|
||||||
|
local claim_name="$1"
|
||||||
|
|
||||||
|
kubectl -n "${NAMESPACE}" patch deployment "${POSTGRES_DEPLOYMENT}" \
|
||||||
|
--type=strategic \
|
||||||
|
-p "{\"spec\":{\"template\":{\"spec\":{\"volumes\":[{\"name\":\"${POSTGRES_VOLUME_NAME}\",\"persistentVolumeClaim\":{\"claimName\":\"${claim_name}\"}}]}}}}" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_on_error() {
|
||||||
|
if [[ "${POSTGRES_CLAIM_SWITCHED}" == "true" ]]; then
|
||||||
|
warn "Migration failed after deployment patch. Switching PostgreSQL back to ${SOURCE_PVC}."
|
||||||
|
patch_postgres_claim "${SOURCE_PVC}" || true
|
||||||
|
POSTGRES_CLAIM_SWITCHED="false"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${SCALED_DOWN_POSTGRES}" == "true" ]]; then
|
||||||
|
warn "Restoring PostgreSQL replicas to ${ORIGINAL_POSTGRES_REPLICAS}."
|
||||||
|
kubectl -n "${NAMESPACE}" scale deployment "${POSTGRES_DEPLOYMENT}" --replicas="${ORIGINAL_POSTGRES_REPLICAS}" >/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${SCALED_DOWN_GITEA}" == "true" ]]; then
|
||||||
|
warn "Restoring Gitea replicas to ${ORIGINAL_GITEA_REPLICAS}."
|
||||||
|
kubectl -n "${NAMESPACE}" scale deployment "${GITEA_DEPLOYMENT}" --replicas="${ORIGINAL_GITEA_REPLICAS}" >/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
on_exit() {
|
||||||
|
local exit_code=$?
|
||||||
|
if [[ ${exit_code} -ne 0 ]]; then
|
||||||
|
restore_on_error
|
||||||
|
warn "Script failed with exit code ${exit_code}."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap on_exit EXIT
|
||||||
|
|
||||||
|
validate_prerequisites() {
|
||||||
|
require_tool kubectl
|
||||||
|
require_file "${TARGET_PVC_MANIFEST}"
|
||||||
|
require_file "${JOB_MANIFEST}"
|
||||||
|
|
||||||
|
kubectl get namespace "${NAMESPACE}" >/dev/null 2>&1 || err "Namespace does not exist: ${NAMESPACE}"
|
||||||
|
kubectl -n "${NAMESPACE}" get deployment "${GITEA_DEPLOYMENT}" >/dev/null 2>&1 || err "Deployment not found: ${GITEA_DEPLOYMENT}"
|
||||||
|
kubectl -n "${NAMESPACE}" get deployment "${POSTGRES_DEPLOYMENT}" >/dev/null 2>&1 || err "Deployment not found: ${POSTGRES_DEPLOYMENT}"
|
||||||
|
kubectl -n "${NAMESPACE}" get pvc "${SOURCE_PVC}" >/dev/null 2>&1 || err "Source PVC not found: ${SOURCE_PVC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm_scale_down() {
|
||||||
|
if [[ "${AUTO_CONFIRM}" == "true" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
warn "Gitea and PostgreSQL in namespace ${NAMESPACE} will be stopped for the migration."
|
||||||
|
read -r -p "Type MIGRATE to continue: " confirmation
|
||||||
|
if [[ "${confirmation}" != "MIGRATE" ]]; then
|
||||||
|
err "Confirmation failed. Aborted by user."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_dry_run_plan() {
|
||||||
|
info "Dry-run mode active. No cluster changes will be made."
|
||||||
|
info "Planned steps:"
|
||||||
|
info "1) Apply ${TARGET_PVC_MANIFEST}"
|
||||||
|
info "2) Wait for PVC ${TARGET_PVC} to be Bound"
|
||||||
|
info "3) Scale deployments ${GITEA_DEPLOYMENT} and ${POSTGRES_DEPLOYMENT} to 0"
|
||||||
|
info "4) Run job ${JOB_NAME} from ${JOB_MANIFEST}"
|
||||||
|
info "5) Verify PG_VERSION and base/ exist on ${TARGET_PVC}"
|
||||||
|
info "6) Patch ${POSTGRES_DEPLOYMENT} to use ${TARGET_PVC}"
|
||||||
|
info "7) Start PostgreSQL and wait until ready"
|
||||||
|
info "8) Start Gitea after PostgreSQL is ready"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_migration() {
|
||||||
|
ORIGINAL_GITEA_REPLICAS="$(kubectl -n "${NAMESPACE}" get deployment "${GITEA_DEPLOYMENT}" -o jsonpath='{.spec.replicas}')"
|
||||||
|
ORIGINAL_POSTGRES_REPLICAS="$(kubectl -n "${NAMESPACE}" get deployment "${POSTGRES_DEPLOYMENT}" -o jsonpath='{.spec.replicas}')"
|
||||||
|
ORIGINAL_GITEA_REPLICAS="${ORIGINAL_GITEA_REPLICAS:-1}"
|
||||||
|
ORIGINAL_POSTGRES_REPLICAS="${ORIGINAL_POSTGRES_REPLICAS:-1}"
|
||||||
|
discover_source_node
|
||||||
|
|
||||||
|
if [[ "${DRY_RUN}" == "true" ]]; then
|
||||||
|
print_dry_run_plan
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
confirm_scale_down
|
||||||
|
|
||||||
|
info "Applying Longhorn target PVC manifest."
|
||||||
|
kubectl apply -f "${TARGET_PVC_MANIFEST}" >/dev/null
|
||||||
|
wait_for_pvc_bound "${TARGET_PVC}" "${PVC_BIND_TIMEOUT_SECONDS}"
|
||||||
|
|
||||||
|
info "Scaling Gitea down first to stop writes."
|
||||||
|
kubectl -n "${NAMESPACE}" scale deployment "${GITEA_DEPLOYMENT}" --replicas=0 >/dev/null
|
||||||
|
SCALED_DOWN_GITEA="true"
|
||||||
|
wait_for_no_pods "app=gitea" "${POD_STOP_TIMEOUT_SECONDS}"
|
||||||
|
|
||||||
|
info "Scaling PostgreSQL down for a consistent filesystem copy."
|
||||||
|
kubectl -n "${NAMESPACE}" scale deployment "${POSTGRES_DEPLOYMENT}" --replicas=0 >/dev/null
|
||||||
|
SCALED_DOWN_POSTGRES="true"
|
||||||
|
wait_for_no_pods "app=postgres" "${POD_STOP_TIMEOUT_SECONDS}"
|
||||||
|
|
||||||
|
info "Recreating migration job ${JOB_NAME}."
|
||||||
|
kubectl -n "${NAMESPACE}" delete job "${JOB_NAME}" --ignore-not-found >/dev/null
|
||||||
|
apply_migration_job
|
||||||
|
|
||||||
|
info "Streaming migration job logs."
|
||||||
|
kubectl -n "${NAMESPACE}" logs -f "job/${JOB_NAME}" || true
|
||||||
|
|
||||||
|
wait_for_job_complete "${JOB_TIMEOUT_SECONDS}"
|
||||||
|
verify_target_data
|
||||||
|
|
||||||
|
info "Switching PostgreSQL deployment to Longhorn PVC ${TARGET_PVC}."
|
||||||
|
patch_postgres_claim "${TARGET_PVC}"
|
||||||
|
POSTGRES_CLAIM_SWITCHED="true"
|
||||||
|
|
||||||
|
info "Starting PostgreSQL on Longhorn."
|
||||||
|
kubectl -n "${NAMESPACE}" scale deployment "${POSTGRES_DEPLOYMENT}" --replicas="${ORIGINAL_POSTGRES_REPLICAS}" >/dev/null
|
||||||
|
SCALED_DOWN_POSTGRES="false"
|
||||||
|
wait_for_deployment_ready "${POSTGRES_DEPLOYMENT}" "${POSTGRES_READY_TIMEOUT_SECONDS}"
|
||||||
|
|
||||||
|
info "Starting Gitea after PostgreSQL verification succeeded."
|
||||||
|
kubectl -n "${NAMESPACE}" scale deployment "${GITEA_DEPLOYMENT}" --replicas="${ORIGINAL_GITEA_REPLICAS}" >/dev/null
|
||||||
|
SCALED_DOWN_GITEA="false"
|
||||||
|
wait_for_deployment_ready "${GITEA_DEPLOYMENT}" "${POSTGRES_READY_TIMEOUT_SECONDS}"
|
||||||
|
|
||||||
|
info "Migration finished successfully."
|
||||||
|
info "Source PVC ${SOURCE_PVC} remains untouched as rollback source."
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
parse_args "$@"
|
||||||
|
info "Starting Gitea PostgreSQL migration from NFS to Longhorn."
|
||||||
|
validate_prerequisites
|
||||||
|
run_migration
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: gitea-postgres-migrate-to-longhorn
|
||||||
|
namespace: gitea
|
||||||
|
spec:
|
||||||
|
backoffLimit: 2
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
name: gitea-postgres-migrate-to-longhorn
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
containers:
|
||||||
|
- name: migrate
|
||||||
|
image: alpine:3.20
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -euo pipefail
|
||||||
|
apk add --no-cache rsync findutils coreutils
|
||||||
|
|
||||||
|
src_count="$(find /source -mindepth 1 | wc -l | tr -d ' ')"
|
||||||
|
echo "Source entries before copy: ${src_count}"
|
||||||
|
|
||||||
|
rsync -aHAX --numeric-ids --delete /source/ /target/
|
||||||
|
|
||||||
|
target_count="$(find /target -mindepth 1 | wc -l | tr -d ' ')"
|
||||||
|
echo "Target entries after copy: ${target_count}"
|
||||||
|
|
||||||
|
test -f /target/PG_VERSION
|
||||||
|
test -d /target/base
|
||||||
|
test "${target_count}" -gt 0
|
||||||
|
|
||||||
|
echo "Top-level target contents:"
|
||||||
|
ls -la /target
|
||||||
|
echo "Migration verification successful"
|
||||||
|
volumeMounts:
|
||||||
|
- name: source
|
||||||
|
mountPath: /source
|
||||||
|
readOnly: true
|
||||||
|
- name: target
|
||||||
|
mountPath: /target
|
||||||
|
volumes:
|
||||||
|
- name: source
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: postgres-pvc
|
||||||
|
- name: target
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: postgres-longhorn-pvc
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: postgres-longhorn-pvc
|
||||||
|
namespace: gitea
|
||||||
|
spec:
|
||||||
|
storageClassName: longhorn
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: homarr
|
||||||
|
namespace: homarr
|
||||||
|
labels:
|
||||||
|
app: homarr
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: homarr
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: homarr
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
fsGroup: 1000
|
||||||
|
containers:
|
||||||
|
- name: homarr
|
||||||
|
image: ghcr.io/homarr-labs/homarr:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
env:
|
||||||
|
- name: SECRET_ENCRYPTION_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: homarr-secret
|
||||||
|
key: SECRET_ENCRYPTION_KEY
|
||||||
|
ports:
|
||||||
|
- containerPort: 7575
|
||||||
|
protocol: TCP
|
||||||
|
volumeMounts:
|
||||||
|
- name: homarr-data
|
||||||
|
mountPath: /appdata
|
||||||
|
- name: homarr-data
|
||||||
|
mountPath: /app/data
|
||||||
|
volumes:
|
||||||
|
- name: homarr-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: homarr-pvc
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: homarr-pvc
|
||||||
|
namespace: homarr
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: homarr-secret
|
||||||
|
namespace: homarr
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
SECRET_ENCRYPTION_KEY: "0fddc6753cb94b4a1dc38f26c52c4d4dbce019237457ede59893fb1a74017512"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: homarr
|
||||||
|
namespace: homarr
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app: homarr
|
||||||
|
ports:
|
||||||
|
- port: 7575
|
||||||
|
targetPort: 7575
|
||||||
|
protocol: TCP
|
||||||
|
nodePort: 30757
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: homarr
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: speedtest-tracker
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: speedtest-tracker
|
||||||
|
namespace: speedtest-tracker
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: speedtest-tracker
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: speedtest-tracker
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: speedtest-tracker
|
||||||
|
spec:
|
||||||
|
dnsPolicy: None
|
||||||
|
dnsConfig:
|
||||||
|
nameservers:
|
||||||
|
- 10.152.183.10
|
||||||
|
- 8.8.8.8
|
||||||
|
searches:
|
||||||
|
- speedtest-tracker.svc.cluster.local
|
||||||
|
options:
|
||||||
|
- name: ndots
|
||||||
|
value: "5"
|
||||||
|
containers:
|
||||||
|
- name: speedtest-tracker
|
||||||
|
image: lscr.io/linuxserver/speedtest-tracker:latest
|
||||||
|
env:
|
||||||
|
- name: PUID
|
||||||
|
value: "1000"
|
||||||
|
- name: PGID
|
||||||
|
value: "1000"
|
||||||
|
- name: APP_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: speedtest-tracker-secret
|
||||||
|
key: APP_KEY
|
||||||
|
- name: DISPLAY_TIMEZONE
|
||||||
|
value: Europe/Berlin
|
||||||
|
- name: DB_CONNECTION
|
||||||
|
value: sqlite
|
||||||
|
- name: SPEEDTEST_SCHEDULE
|
||||||
|
value: "0 * * * *"
|
||||||
|
- name: SPEEDTEST_SERVERS
|
||||||
|
value: ""
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /config
|
||||||
|
name: speedtest-tracker
|
||||||
|
volumes:
|
||||||
|
- name: speedtest-tracker
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: speedtest-tracker
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: speedtest-tracker
|
||||||
|
namespace: speedtest-tracker
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
rules:
|
||||||
|
- host: speedtest.henryathome.home64.de
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: speedtest-tracker
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: speedtest-tracker
|
||||||
|
namespace: speedtest-tracker
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: speedtest-tracker-secret
|
||||||
|
namespace: speedtest-tracker
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
APP_KEY: "base64:kRwkJqieSmtYw0+066zNiNgXInLSexYxT9RgIyONNMI=" # https://speedtest-tracker.dev
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: speedtest-tracker
|
||||||
|
namespace: speedtest-tracker
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: speedtest-tracker
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: speedtest-tracker
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
|
protocol: TCP
|
||||||
|
nodePort: 30800
|
||||||
Reference in New Issue
Block a user