#!/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 "$@"