#!/usr/bin/env bash set -euo pipefail PROG="init_gitea" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck disable=SC1090 source "$SCRIPT_DIR/knoe_cfg.sh" MODE="$(knoe_normalize_mode "${KNOE_MODE:-${DEPLOYMENT_MODE:-k3d}}")" NAMESPACE="${GITEA_NAMESPACE:-}" CFG_PATH="" FORCE=0 usage() { cat < Deployment mode (default: ${MODE:-k3d}) -n, --namespace Target namespace (default: gitea) -c, --config Path to config file (defaults to detected) --force Delete existing release before deploy --help Show this help Behavior: - Prefers Helm install/upgrade (gitea-charts/gitea). - Falls back to applying a generated Deployment/Service/Ingress manifest. - If a local registry is enabled, the image is mirrored there first. EOF } die() { echo "[ERROR] $*" >&2; exit 2; } log() { echo "[INFO] $*"; } warn() { echo "[WARN] $*" >&2; } while [[ $# -gt 0 ]]; do case "$1" in --mode) MODE="$(knoe_normalize_mode "${2:-}")"; shift 2 ;; --mode=*) MODE="$(knoe_normalize_mode "${1#*=}")"; shift 1 ;; -n|--namespace) NAMESPACE="${2:-}"; shift 2 ;; --namespace=*) NAMESPACE="${1#*=}"; shift 1 ;; -c|--config) CFG_PATH="${2:-}"; shift 2 ;; --config=*) CFG_PATH="${1#*=}"; shift 1 ;; --force) FORCE=1; shift ;; -h|--help) usage; exit 0 ;; *) break ;; esac done # Resolve config path and namespace defaults when present if [[ -z "$CFG_PATH" && -n "${KNOE_CONF:-}" ]]; then CFG_PATH="$(_knoe_cfg_select_cfg_file "${KNOE_CONF}")" fi if [[ -z "$CFG_PATH" ]]; then CFG_PATH="$(_knoe_cfg_select_cfg_file "$SCRIPT_DIR/../conf")" fi if [[ -z "$NAMESPACE" && -n "$CFG_PATH" ]]; then maybe_ns="$(_knoe_cfg_extract_key "$CFG_PATH" "GITOPS_NAMESPACE")" [[ -z "$maybe_ns" ]] && maybe_ns="$(_knoe_cfg_extract_key "$CFG_PATH" "GITEA_NAMESPACE")" NAMESPACE="$maybe_ns" fi NAMESPACE="${NAMESPACE:-gitea}" export GITEA_NAMESPACE="$NAMESPACE" case "$MODE" in k3d|k3s|k8s|local) ;; *) die "Unsupported mode '$MODE' (use k3d, k3s, k8s, or local)" ;; esac export KNOE_MODE="$MODE" command -v kubectl >/dev/null || die "kubectl not found" command -v helm >/dev/null || warn "helm not found — will use manifest fallback" kubectl get ns "$NAMESPACE" >/dev/null 2>&1 || kubectl create namespace "$NAMESPACE" >/dev/null 2>&1 CHART_REPO="https://dl.gitea.com/charts" CHART_NAME="gitea-charts/gitea" RELEASE_NAME="gitea" IMAGE_REPO_DEFAULT="gitea/gitea" IMAGE_TAG="${GITEA_IMAGE_TAG:-1.22.3}" IMAGE_REPO="$IMAGE_REPO_DEFAULT" NODE_SELECTOR="${GITEA_NODE_SELECTOR:-${NODE_SELECTOR:-}}" NODE_SELECTOR_KEY="${GITEA_NODE_SELECTOR_KEY:-${NODE_SELECTOR_KEY:-kubernetes.io/hostname}}" GITEA_PV_NODE_SELECTOR_KEY="${GITEA_PV_NODE_SELECTOR_KEY:-${NODE_SELECTOR_KEY}}" GITEA_PV_NODE="${GITEA_PV_NODE:-${GITEA_NODE_SELECTOR:-}}" GITEA_PV_BASE_DIR="${GITEA_PV_BASE_DIR:-/synology/d005}" GITEA_STORAGE_CLASS="${GITEA_STORAGE_CLASS:-gitea-local-d005}" GITEA_DOMAIN="${GITEA_DOMAIN:-git.prole.org}" GITEA_SSH_DOMAIN="${GITEA_SSH_DOMAIN:-$GITEA_DOMAIN}" KNOE_DB_NAMESPACE="${KNOE_DB_NAMESPACE:-${DATABASE_NAMESPACE:-knoe-db}}" KNOE_DB_CLUSTER="${KNOE_DB_CLUSTER:-${CLUSTER_NAME:-knoe-db}}" KNOE_DB_SERVICE="${KNOE_DB_SERVICE:-${KNOE_DB_CLUSTER}-rw}" KNOE_DB_PORT="${KNOE_DB_PORT:-${DB_HOST_PORT:-5432}}" KNOE_DB_ADMIN_USER="${KNOE_DB_ADMIN_USER:-${KNOE_DB_USER:-postgres}}" GITEA_DB_NAME="${GITEA_DB_NAME:-gitea}" GITEA_DB_USER="${GITEA_DB_USER:-gitea}" GITEA_DB_PASSWORD="${GITEA_DB_PASSWORD:-${DB_PASSWORD:-}}" if [[ "$MODE" == "k3s" && -z "$GITEA_PV_NODE" ]]; then die "GITEA_PV_NODE (or GITEA_NODE_SELECTOR) must be set in k3s mode." fi is_secret_placeholder() { case "${1:-}" in '${KNOE_SECRET:'*|'${OPENBAO:'*) return 0 ;; esac return 1 } resolve_gitea_db_password() { if [[ -n "${GITEA_DB_PASSWORD:-}" ]] && ! is_secret_placeholder "${GITEA_DB_PASSWORD}"; then return 0 fi if kubectl -n "$KNOE_DB_NAMESPACE" get secret knoe-db-superuser >/dev/null 2>&1; then local resolved resolved=$(kubectl -n "$KNOE_DB_NAMESPACE" get secret knoe-db-superuser \ -o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null || true) if [[ -n "$resolved" ]]; then GITEA_DB_PASSWORD="$resolved" fi fi if [[ -z "${GITEA_DB_PASSWORD:-}" ]] || is_secret_placeholder "${GITEA_DB_PASSWORD}"; then warn "Could not resolve a concrete Gitea DB password; database bootstrap may be skipped." fi } resolve_knoe_db_primary_pod() { local primary primary=$(kubectl -n "$KNOE_DB_NAMESPACE" get pods \ -l "cnpg.io/cluster=${KNOE_DB_CLUSTER},cnpg.io/instanceRole=primary" \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) if [[ -z "$primary" ]]; then primary=$(kubectl -n "$KNOE_DB_NAMESPACE" get pods \ -l "cnpg.io/cluster=${KNOE_DB_CLUSTER}" \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) fi printf '%s' "$primary" } sql_escape_literal() { printf '%s' "${1:-}" | sed "s/'/''/g" } setup_knoe_db_for_gitea() { local primary primary="$(resolve_knoe_db_primary_pod)" if [[ -z "$primary" ]]; then warn "No knoe-db pod found in namespace '${KNOE_DB_NAMESPACE}'; skipping Gitea DB setup." return 0 fi resolve_gitea_db_password if [[ -z "${GITEA_DB_PASSWORD:-}" ]] || is_secret_placeholder "${GITEA_DB_PASSWORD}"; then warn "Skipping Gitea DB setup due to unresolved GITEA_DB_PASSWORD." return 0 fi local admin_user="" local candidate for candidate in "$KNOE_DB_ADMIN_USER" postgres root; do [[ -z "$candidate" ]] && continue if kubectl -n "$KNOE_DB_NAMESPACE" exec "$primary" -c postgres -- \ psql -U "$candidate" -d postgres -tAc "SELECT 1" >/dev/null 2>&1; then admin_user="$candidate" break fi done if [[ -z "$admin_user" ]]; then warn "Unable to connect to knoe-db as admin user; skipping Gitea DB setup." return 0 fi local escaped_password escaped_password="$(sql_escape_literal "$GITEA_DB_PASSWORD")" kubectl -n "$KNOE_DB_NAMESPACE" exec "$primary" -c postgres -- \ psql -U "$admin_user" -d postgres -c " DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='${GITEA_DB_USER}') THEN CREATE ROLE ${GITEA_DB_USER} LOGIN PASSWORD '${escaped_password}'; END IF; END \$\$; " >/dev/null 2>&1 || warn "Could not create role '${GITEA_DB_USER}'." local db_exists db_exists=$(kubectl -n "$KNOE_DB_NAMESPACE" exec "$primary" -c postgres -- \ psql -U "$admin_user" -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='${GITEA_DB_NAME}';" 2>/dev/null || true) if [[ "$db_exists" != "1" ]]; then kubectl -n "$KNOE_DB_NAMESPACE" exec "$primary" -c postgres -- \ psql -U "$admin_user" -d postgres -c "CREATE DATABASE ${GITEA_DB_NAME} OWNER ${GITEA_DB_USER};" \ >/dev/null 2>&1 || warn "Could not create database '${GITEA_DB_NAME}'." fi log "Gitea database '${GITEA_DB_NAME}' prepared in namespace '${KNOE_DB_NAMESPACE}'." } # Optional reset if [[ "$FORCE" -eq 1 ]]; then warn "--force specified; removing existing Gitea resources in namespace $NAMESPACE" if command -v helm >/dev/null 2>&1; then helm uninstall "$RELEASE_NAME" -n "$NAMESPACE" >/dev/null 2>&1 || true fi kubectl -n "$NAMESPACE" delete deploy/gitea svc/gitea-http svc/gitea-ssh ingress/gitea >/dev/null 2>&1 || true fi # Mirror to local registry if enabled if _knoe_local_registry_enabled && command -v docker >/dev/null; then HOST_REG="${LOCAL_REGISTRY:-${PROLE_LOCAL_REGISTRY:-}}" INTERNAL_REG="${LOCAL_REGISTRY_INTERNAL:-${PROLE_LOCAL_REGISTRY_INTERNAL:-}}" if [[ -n "$HOST_REG" ]]; then HOST_IMG="${HOST_REG}/${IMAGE_REPO_DEFAULT}" log "Pulling ${IMAGE_REPO_DEFAULT}:${IMAGE_TAG}" docker pull "${IMAGE_REPO_DEFAULT}:${IMAGE_TAG}" >/dev/null log "Tagging ${HOST_IMG}:${IMAGE_TAG}" docker tag "${IMAGE_REPO_DEFAULT}:${IMAGE_TAG}" "${HOST_IMG}:${IMAGE_TAG}" log "Pushing ${HOST_IMG}:${IMAGE_TAG}" docker push "${HOST_IMG}:${IMAGE_TAG}" >/dev/null || warn "Push to local registry failed; continuing with upstream image" if [[ -n "$INTERNAL_REG" ]]; then IMAGE_REPO="${INTERNAL_REG}/gitea/gitea" else IMAGE_REPO="${HOST_REG}/gitea/gitea" fi fi fi deploy_with_helm() { command -v helm >/dev/null 2>&1 || return 1 helm repo add gitea-charts "$CHART_REPO" >/dev/null 2>&1 || true helm repo update >/dev/null 2>&1 || true # Minimal config: NodePort or ClusterIP + Ingress depending on environment. # Avoid heavy persistence defaults; users can override via values later. local -a helm_args=( --set "image.repository=$IMAGE_REPO" --set "image.tag=$IMAGE_TAG" --set "image.rootless=false" --set "image.fullOverride=${IMAGE_REPO}:${IMAGE_TAG}" --set "service.http.type=ClusterIP" --set "service.ssh.type=ClusterIP" --set "service.ssh.port=22" --set "gitea.admin.username=${GITEA_ADMIN_USER:-gitea_admin}" --set "gitea.admin.password=${GITEA_ADMIN_PASSWORD:-gitea_admin}" --set "gitea.admin.email=${GITEA_ADMIN_EMAIL:-gitea_admin@example.local}" --set "gitea.config.server.DOMAIN=${GITEA_DOMAIN}" --set "gitea.config.server.SSH_DOMAIN=${GITEA_SSH_DOMAIN}" --set "gitea.config.server.ROOT_URL=http://${GITEA_DOMAIN}/" --set "gitea.config.server.HTTP_PORT=3000" --set "gitea.config.server.SSH_PORT=22" --set "gitea.config.database.DB_TYPE=postgres" --set "gitea.config.database.HOST=${KNOE_DB_SERVICE}.${KNOE_DB_NAMESPACE}.svc.cluster.local:${KNOE_DB_PORT}" --set "gitea.config.database.NAME=${GITEA_DB_NAME}" --set "gitea.config.database.USER=${GITEA_DB_USER}" --set-string "gitea.config.database.PASSWD=${GITEA_DB_PASSWORD}" --set "gitea.config.database.SSL_MODE=disable" --set "gitea.config.session.PROVIDER=db" --set "postgresql.enabled=false" --set "postgresql-ha.enabled=false" --set "valkey-cluster.enabled=false" --set "persistence.enabled=true" ) if [[ "$MODE" == "k3s" ]]; then helm_args+=( --set "gitea.config.cache.ADAPTER=memory" --set "global.storageClass=$GITEA_STORAGE_CLASS" --set "persistence.storageClass=$GITEA_STORAGE_CLASS" --set-json "nodeSelector={\"${GITEA_PV_NODE_SELECTOR_KEY}\":\"${GITEA_PV_NODE}\"}" --set "deployment.strategy=Recreate" ) fi helm upgrade --install "$RELEASE_NAME" "$CHART_NAME" \ -n "$NAMESPACE" \ "${helm_args[@]}" \ --wait --timeout 10m } ensure_k3s_storage_layout() { [[ "$MODE" == "k3s" ]] || return 0 log "Ensuring Gitea k3s storage on node '${GITEA_PV_NODE}' at '${GITEA_PV_BASE_DIR}' (storageClass=${GITEA_STORAGE_CLASS})" cat </dev/null 2>&1 || true cat </dev/null kubectl -n "$NAMESPACE" delete pod "$prep_pod" --ignore-not-found >/dev/null 2>&1 || true } reset_stuck_k3s_release() { [[ "$MODE" == "k3s" ]] || return 0 local pending pending=$(kubectl -n "$NAMESPACE" get pvc -o jsonpath='{range .items[*]}{.spec.storageClassName}:{.status.phase}{"\n"}{end}' 2>/dev/null | grep '^:Pending$' || true) if [[ -z "$pending" ]]; then return 0 fi warn "Detected pending PVCs without storageClass in namespace ${NAMESPACE}; resetting stuck Gitea release" if command -v helm >/dev/null 2>&1; then helm uninstall "$RELEASE_NAME" -n "$NAMESPACE" >/dev/null 2>&1 || true fi kubectl -n "$NAMESPACE" delete pvc gitea-shared-storage data-gitea-postgresql-ha-postgresql-0 data-gitea-postgresql-ha-postgresql-1 data-gitea-postgresql-ha-postgresql-2 valkey-data-gitea-valkey-cluster-0 valkey-data-gitea-valkey-cluster-1 valkey-data-gitea-valkey-cluster-2 >/dev/null 2>&1 || true } release_exists() { command -v helm >/dev/null 2>&1 && helm status "$RELEASE_NAME" -n "$NAMESPACE" >/dev/null 2>&1 } gitea_workload_exists() { kubectl -n "$NAMESPACE" get deploy "$RELEASE_NAME" >/dev/null 2>&1 } apply_manifest_fallback() { if release_exists || gitea_workload_exists; then warn "Helm resources detected for release '$RELEASE_NAME'; skipping fallback manifest to avoid immutable selector/port conflicts" return 0 fi warn "Applying fallback manifest" local node_selector_block="" if [[ -n "$NODE_SELECTOR" ]]; then node_selector_block=$(cat </dev/null 2>&1 || warn "gitea-spnego-proxy manifest apply failed (may already be up-to-date)" else warn "gitea-spnego-proxy manifest not found at $spnego_manifest — skipping" fi if [[ "$MODE" != "k3s" ]]; then warn "SPNEGO keytab provisioning via Ansible only runs in k3s mode (current: $MODE)." warn "For other modes, manually run:" warn " ansible-playbook infrastructure/playbooks/gitea_spnego_keytab.yml" return 0 fi if ! command -v ansible-playbook >/dev/null 2>&1; then warn "ansible-playbook not found — skipping SPNEGO keytab provisioning." warn "Run manually: ansible-playbook infrastructure/playbooks/gitea_spnego_keytab.yml" return 0 fi local playbook="${SCRIPT_DIR}/../infrastructure/playbooks/gitea_spnego_keytab.yml" if [[ ! -f "$playbook" ]]; then warn "Playbook not found at $playbook — skipping SPNEGO keytab provisioning" return 0 fi log "Running SPNEGO keytab provisioning via Ansible..." if ansible-playbook "$playbook" \ -e "gitea_kube_context=${KUBECONTEXT:-prole-service-cluster}" \ -e "gitea_spnego_namespace=${NAMESPACE}"; then log "✓ Gitea SPNEGO keytab provisioned — SPNEGO challenge active on git.prole.org" else warn "SPNEGO keytab provisioning failed. Gitea will still work via its own login page." warn "Retry: ansible-playbook infrastructure/playbooks/gitea_spnego_keytab.yml -e force_keytab_reset=true" fi } ACTION="${1:-deploy}" case "$ACTION" in deploy|"") ;; *) usage; die "Unknown action '$ACTION'" ;; esac log "Deploying Gitea to namespace '$NAMESPACE' (mode=$MODE)" reset_stuck_k3s_release ensure_k3s_storage_layout prepare_k3s_host_permissions setup_knoe_db_for_gitea if deploy_with_helm; then log "Gitea deployed via Helm" else apply_manifest_fallback log "Gitea deployed via manifest fallback" fi log "Waiting for deployment rollout..." kubectl -n "$NAMESPACE" rollout status deploy/gitea --timeout=10m # Provision the SPNEGO proxy keytab and verify the Negotiate challenge is live. # This step is idempotent — safe to run on upgrades/re-deploys. setup_gitea_spnego log "Done. Service endpoints:" kubectl -n "$NAMESPACE" get svc gitea-http gitea-ssh