ADD: implement Gitea persistent volume and claim; migrate PostgreSQL from NFS to Longhorn with updated configurations

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-25 20:34:59 +02:00
parent 9905abd9b4
commit 76945105d7
5 changed files with 39 additions and 465 deletions
+27
View File
@@ -0,0 +1,27 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: gitea-pv-target
spec:
storageClassName: nfs
capacity:
storage: 30Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
server: 192.168.178.186
path: /volume1/giteaRepos
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitea-pvc-target
namespace: gitea
spec:
storageClassName: nfs
accessModes:
- ReadWriteMany
resources:
requests:
storage: 30Gi
+12 -68
View File
@@ -5,72 +5,12 @@ kind: Namespace
metadata:
name: gitea
# PV + PVC: Gitea (NFS)
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: gitea-pv
spec:
storageClassName: nfs
capacity:
storage: 30Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
server: 192.168.178.166
path: /export/fastData/gitea/repos
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitea-pvc
namespace: gitea
spec:
storageClassName: nfs
accessModes:
- ReadWriteMany
resources:
requests:
storage: 30Gi
# PV + PVC: PostgreSQL (NFS)
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: postgres-pv
spec:
storageClassName: nfs
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
nfs:
server: 192.168.178.166
path: /export/fastData/gitea/postgres
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: gitea
spec:
storageClassName: nfs
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
# PVC: PostgreSQL (Longhorn target)
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-longhorn-pvc
name: postgres-longhorn-pvc-3g
namespace: gitea
spec:
storageClassName: longhorn
@@ -78,7 +18,7 @@ spec:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storage: 3Gi
# Deployment: PostgreSQL
---
@@ -119,7 +59,7 @@ spec:
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-longhorn-pvc
claimName: postgres-longhorn-pvc-3g
# Service: PostgreSQL
---
@@ -153,14 +93,16 @@ spec:
labels:
app: gitea
spec:
nodeSelector:
kubernetes.io/hostname: knode1
containers:
- name: gitea
image: gitea/gitea:latest
env:
- name: USER_UID
value: "1000"
value: "1024"
- name: USER_GID
value: "1000"
value: "100"
- name: GITEA__database__DB_TYPE
value: postgres
- name: GITEA__database__HOST
@@ -187,18 +129,20 @@ spec:
value: "32000"
- name: GITEA__packages__ENABLED
value: "true"
- name: GITEA__repository__ROOT
value: /data/gitea/git/repositories
- name: GITEA__lfs__PATH
value: /data/gitea/git/lfs
ports:
- containerPort: 3000 # HTTP
- containerPort: 32000 # SSH
volumeMounts:
- name: gitea-storage
mountPath: /data
securityContext:
fsGroup: 1000
volumes:
- name: gitea-storage
persistentVolumeClaim:
claimName: gitea-pvc
claimName: gitea-pvc-target
# Service: Gitea (inkl. SSH)
---
@@ -1,335 +0,0 @@
#!/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 "$@"
@@ -1,50 +0,0 @@
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
-12
View File
@@ -1,12 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-longhorn-pvc
namespace: gitea
spec:
storageClassName: longhorn
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi