prole/gitea/deploy.sh
chrisfu dbe249ce9f fix(gitea): REST API bootstrap path bypasses broken admin CLI; fix DB namespace resolver
deploy.sh: resolve_knoe_db_namespace() now checks DATABASE_NAMESPACE from
config (k3s.cfg: knoe-db) before probing namespaces. Previously it fell back
to 'default' because only 'knoe' was probed, baking a wrong DB hostname into
app.ini. The running server was unaffected (uses GITEA__database__HOST env var)
but the gitea admin CLI, which reads app.ini directly, could not connect.

init_knoe_users.sh: add gitea_helm_admin_token() which reads the Helm
bootstrap admin password from the gitea-gitea k8s secret and exchanges it for
a REST API token — entirely avoiding the broken CLI. Add gitea_api_set_password()
which uses PATCH /api/v1/admin/users/{user} to set the password via the running
web server (which has the correct DB connection) instead of kubectl exec.

promote_gitea_admin() now prefers the REST-API-only path:
  1. existing token (env / k8s secret)
  2. Helm admin k8s secret → REST token; set KNOE_ADMIN_PRINCIPAL password via API
  3. 1Password → CLI set-password → basic-auth token (fallback)
  4. kubectl exec generate-access-token (last resort)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 18:53:55 -07:00

1183 lines
31 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
if [[ -f "$PROJECT_ROOT/env.sh" ]]; then
# shellcheck disable=SC1090
source "$PROJECT_ROOT/env.sh"
fi
MODE="k3d"
HELM_RELEASE="gitea"
USE_DEV_HELPERS="false"
FOREGROUND="false"
FORCE="true"
PROLE_CFG_PATH=""
HELM_TEMPLATE_ONLY="false"
usage() {
cat <<'USAGE'
gitea/deploy.sh
Launches the full Gitea open-source stack locally, following the
README/DEVELOPERS guidance (Docker Compose). This is a multi-service deployment
(Postgres, Auth, Storage, Realtime, Kong, Studio, etc.)
Usage:
./deploy.sh [options]
Options:
--mode <local|k3d|k8s> Deployment mode ('local' for Docker Compose, 'k3d' for local k3d, 'k8s' for generic k8s/k3s; default: k3d)
--helm-template-only Render Helm manifests to gitea/k8s without applying (for pipelines)
--with-dev-helpers Include docker/dev/docker-compose.dev.yml
--foreground Run docker compose in the foreground (default: detached, local mode only)
-f, --force Reset the gitea namespace before applying manifests (k3d only; default)
--no-force Skip namespace reset (k3d only)
-c, --config <path> Path to config file (loads environment defaults)
-h, --help Show help
Notes:
- This script syncs the gitea repo into $DEV_HOME/gitea.
- For k3d mode, manifests are applied from $GITEA_K8S_DIR.
- Docker image artifacts are cached in $DOCKER_IMPORT_DIR.
- steps:
1) use docker/docker-compose.yml
2) copy docker/.env.example -> docker/.env (if missing)
3) run docker compose up
USAGE
}
die() {
echo "Error: $*" >&2
exit 1
}
warn() {
echo "Warning: $*" >&2
}
log() {
echo "==> $*"
}
apply_defaults() {
DEV_HOME="${DEV_HOME:-$HOME/dev}"
DEV_HOME="${DEV_HOME/#\~/$HOME}"
GITEA_DIR="$DEV_HOME/gitea"
DOCKER_DIR="$GITEA_DIR/docker"
COMPOSE_FILE="$DOCKER_DIR/docker-compose.yml"
DEV_COMPOSE_FILE="$DOCKER_DIR/dev/docker-compose.dev.yml"
ENV_EXAMPLE="$DOCKER_DIR/.env.example"
ENV_FILE="$DOCKER_DIR/.env"
GITEA_K8S_DIR="${GITEA_K8S_DIR:-$SCRIPT_DIR/k8s}"
local default_owner
default_owner="$(id -un 2>/dev/null || echo knoe)"
local data_root="${PROLE_DATA:-/opt/knoe/data/${default_owner}}"
data_root="${data_root%/}"
DOCKER_IMPORT_DIR="${DOCKER_IMPORT_DIR:-$data_root/docker-import}"
GITEA_IMAGE_PLATFORM="${GITEA_IMAGE_PLATFORM:-}"
GITEA_IMAGE_PLATFORMS="${GITEA_IMAGE_PLATFORMS:-linux/amd64 linux/arm64}"
GITEA_IMAGE_TAG="${GITEA_IMAGE_TAG:-1.22.3}"
GITEA_DOMAIN="${GITEA_DOMAIN:-git.knoe.org}"
GITEA_SSH_DOMAIN="${GITEA_SSH_DOMAIN:-git.knoe.org}"
GITEA_NAMESPACE="${GITEA_NAMESPACE:-gitea}"
KNOE_DB_SERVICE="${KNOE_DB_SERVICE:-knoe-db-rw}"
KNOE_DB_NAMESPACE="${KNOE_DB_NAMESPACE:-}"
AUTHORITY_NAMESPACE="${AUTHORITY_NAMESPACE:-authority}"
AUTHORITY_SERVICE="${AUTHORITY_SERVICE:-authority-ldap}"
AUTHORITY_LDAP_PORT="${AUTHORITY_LDAP_PORT:-389}"
AUTHORITY_BASE_DN="${AUTHORITY_BASE_DN:-dc=knoe,dc=org}"
AUTHORITY_BIND_DN="${AUTHORITY_BIND_DN:-cn=gitea,ou=services,dc=knoe,dc=org}"
AUTHORITY_BIND_PASSWORD="${AUTHORITY_BIND_PASSWORD:-}"
}
load_knoe_cfg() {
local cfg_loader="$PROJECT_ROOT/etc/knoe_cfg.sh"
local cfg_path="${PROLE_CFG_PATH:-}"
local candidate=""
if [[ -z "$cfg_path" ]]; then
if [[ -n "${KNOE_CONF:-}" ]]; then
for candidate in \
"${KNOE_CONF%/}/k3d.cfg" \
"${KNOE_CONF%/}/k3s.cfg" \
"${KNOE_CONF%/}/gke.cfg" \
"${KNOE_CONF%/}/knoe.cfg"; do
if [[ -f "$candidate" ]]; then
cfg_path="$candidate"
break
fi
done
elif [[ -f "$PROJECT_ROOT/conf/k3d.cfg" ]]; then
cfg_path="$PROJECT_ROOT/conf/k3d.cfg"
elif [[ -f "$PROJECT_ROOT/conf/k3s.cfg" ]]; then
cfg_path="$PROJECT_ROOT/conf/k3s.cfg"
elif [[ -f "$PROJECT_ROOT/conf/gke.cfg" ]]; then
cfg_path="$PROJECT_ROOT/conf/gke.cfg"
elif [[ -f "$PROJECT_ROOT/conf/knoe.cfg" ]]; then
cfg_path="$PROJECT_ROOT/conf/knoe.cfg"
fi
fi
if [[ -z "$cfg_path" ]]; then
return 0
fi
if [[ -n "$cfg_path" ]]; then
if [[ -d "$cfg_path" ]]; then
cfg_path="$cfg_path/knoe.cfg"
fi
[[ -f "$cfg_path" ]] || die "Config file not found: $cfg_path"
KNOE_CONF="$(cd "$(dirname "$cfg_path")" && pwd)"
export KNOE_CONF
PROLE_CFG_PATH="$cfg_path"
fi
if [[ -f "$cfg_loader" ]]; then
# shellcheck disable=SC1090
source "$cfg_loader"
else
warn "knoe_cfg.sh not found; config defaults may be incomplete."
fi
}
register_gitea_ports() {
if command -v knoe_register_port_forward >/dev/null 2>&1; then
knoe_register_port_forward "gitea-http" "$GITEA_NAMESPACE" "svc/gitea-http" "3000" "3000" "0.0.0.0" "TCP" "Gitea Web UI"
knoe_register_port_forward "gitea-ssh" "$GITEA_NAMESPACE" "svc/gitea-ssh" "2222" "22" "0.0.0.0" "TCP" "Gitea SSH"
fi
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
}
detect_platform() {
case "$(uname -m 2>/dev/null || true)" in
arm64|aarch64) echo "linux/arm64" ;;
x86_64|amd64) echo "linux/amd64" ;;
*) echo "linux/amd64" ;;
esac
}
normalize_platform() {
case "${1:-}" in
linux/*) echo "$1" ;;
arm64|aarch64) echo "linux/arm64" ;;
x86_64|amd64) echo "linux/amd64" ;;
"") detect_platform ;;
*) detect_platform ;;
esac
}
compose_cmd() {
if docker compose version >/dev/null 2>&1; then
echo "docker compose"
return 0
fi
if command -v docker-compose >/dev/null 2>&1; then
echo "docker-compose"
return 0
fi
die "Docker Compose not found (expected 'docker compose' or 'docker-compose')"
}
ensure_k3d() {
require_cmd k3d
}
ensure_helm() {
require_cmd helm
}
list_k3d_clusters() {
local json=""
if json="$(k3d cluster list -o json 2>/dev/null)"; then
if [[ -n "$json" ]]; then
if printf '%s' "$json" | python3 - <<'PY'
import json
import sys
raw = sys.stdin.read()
if not raw.strip():
sys.exit(1)
try:
data = json.loads(raw)
except Exception:
sys.exit(1)
items = data.get("items") if isinstance(data, dict) else data
if not items:
sys.exit(0)
for item in items:
name = item.get("name") if isinstance(item, dict) else None
if name:
print(name)
PY
then
return 0
fi
fi
fi
k3d cluster list 2>/dev/null | awk 'NR>1 {print $1}'
}
cluster_exists() {
local name="$1"
list_k3d_clusters | awk -v target="$name" '$0 == target {found=1} END {exit found ? 0 : 1}'
}
ensure_k3d_cluster() {
local cluster="${K3D_CLUSTER_NAME:-}"
local -a clusters
local picked=""
mapfile -t clusters < <(list_k3d_clusters || true)
if [[ -n "$cluster" ]]; then
if ! cluster_exists "$cluster"; then
log "Creating k3d cluster '$cluster'..."
k3d cluster create "$cluster" >/dev/null
fi
picked="$cluster"
elif [[ ${#clusters[@]} -gt 0 ]]; then
for name in "${clusters[@]}"; do
if [[ "$name" == "k3s-default" ]]; then
picked="$name"
break
fi
done
if [[ -z "$picked" ]]; then
picked="${clusters[0]}"
fi
else
picked="k3s-default"
log "Creating k3d cluster '$picked'..."
k3d cluster create "$picked" >/dev/null
fi
export K3D_CLUSTER_NAME="$picked"
log "Using k3d cluster: $K3D_CLUSTER_NAME"
k3d cluster start "$K3D_CLUSTER_NAME" >/dev/null 2>&1 || true
k3d kubeconfig merge "$K3D_CLUSTER_NAME" --switch-context >/dev/null 2>&1 || true
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--mode|-m)
MODE="${2:-}"
shift 2
;;
-c|--config)
PROLE_CFG_PATH="${2:-}"
[[ -n "$PROLE_CFG_PATH" ]] || die "Missing value for $1"
shift 2
;;
--config=*)
PROLE_CFG_PATH="${1#*=}"
[[ -n "$PROLE_CFG_PATH" ]] || die "Missing value for $1"
shift
;;
--with-dev-helpers)
USE_DEV_HELPERS="true"
shift
;;
--helm-template-only)
HELM_TEMPLATE_ONLY="true"
shift
;;
--foreground)
FOREGROUND="true"
shift
;;
-f|--force)
FORCE="true"
shift
;;
--no-force)
FORCE="false"
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "Unknown option: $1"
;;
esac
done
}
ensure_env_file() {
if [[ -f "$ENV_FILE" ]]; then
return 0
fi
[[ -f "$ENV_EXAMPLE" ]] || die "Missing env example file: $ENV_EXAMPLE"
log "Creating docker/.env from docker/.env.example"
cp "$ENV_EXAMPLE" "$ENV_FILE"
warn "docker/.env contains default secrets. Update them before any production use."
}
compose_files() {
local -a files=("-f" "docker-compose.yml")
if [[ "$USE_DEV_HELPERS" == "true" ]]; then
[[ -f "$DEV_COMPOSE_FILE" ]] || die "Missing dev helpers compose file: $DEV_COMPOSE_FILE"
files+=("-f" "dev/docker-compose.dev.yml")
fi
echo "${files[@]}"
}
ensure_repo() {
require_cmd git
mkdir -p "$DEV_HOME"
if [[ -d "$GITEA_DIR/.git" ]]; then
log "Updating Gitea repo in $GITEA_DIR"
(cd "$GITEA_DIR" && git pull)
return 0
fi
if [[ -e "$GITEA_DIR" ]]; then
die "Path exists but is not a git repo: $GITEA_DIR"
fi
log "Cloning Gitea repo into $GITEA_DIR"
git clone https://github.com/go-gitea/gitea.git "$GITEA_DIR"
}
run_local() {
ensure_repo
[[ -f "$COMPOSE_FILE" ]] || die "Missing compose file: $COMPOSE_FILE"
require_cmd docker
docker info >/dev/null 2>&1 || die "Docker daemon is not running"
local compose
compose="$(compose_cmd)"
log "Knoe::Gitea local deploy (Docker Compose)"
log "Repo: $GITEA_DIR"
log "Namespace: gitea (Compose project name)"
ensure_env_file
local compose_args=("-f" "docker-compose.yml")
if [[ "$USE_DEV_HELPERS" == "true" ]]; then
[[ -f "$DEV_COMPOSE_FILE" ]] || die "Missing dev helpers compose file: $DEV_COMPOSE_FILE"
compose_args+=("-f" "dev/docker-compose.dev.yml")
fi
local up_args=("up")
if [[ "$FOREGROUND" != "true" ]]; then
up_args+=("-d")
fi
log "Starting Gitea services..."
(cd "$DOCKER_DIR" && COMPOSE_PROJECT_NAME="gitea" $compose "${compose_args[@]}" "${up_args[@]}")
log "Deployment complete."
log "Access Gitea at http://localhost:3000"
}
image_safe_name() {
echo "$1" | sed 's/[\/:@]/_/g'
}
image_platform_tag() {
local img="$1"
local platform="$2"
local suffix="${platform//\//-}"
local name tag
if [[ "$img" == *@* ]]; then
name="${img%@*}"
tag="digest-${suffix}"
elif [[ "$img" == *:* ]]; then
name="${img%:*}"
tag="${img##*:}-${suffix}"
else
name="$img"
tag="latest-${suffix}"
fi
echo "${name}:${tag}"
}
ensure_image_artifact() {
local img="$1"
local platform="$2"
local safe_name
local tar_path
local platform_tag
safe_name="$(image_safe_name "$img")"
tar_path="${DOCKER_IMPORT_DIR}/${safe_name}.${platform//\//-}.tar"
platform_tag="$(image_platform_tag "$img" "$platform")"
if [[ -f "$tar_path" ]]; then
return 0
fi
mkdir -p "$DOCKER_IMPORT_DIR"
if docker buildx version >/dev/null 2>&1; then
log "Fetching image $img for platform $platform (buildx export)"
if ! printf 'FROM %s\n' "$img" \
| docker buildx build --pull --platform "$platform" -t "$platform_tag" \
--output "type=docker,dest=$tar_path" - >/dev/null 2>&1; then
die "Failed to export image $img ($platform) via buildx"
fi
return 0
fi
log "Fetching image $img for platform $platform"
if ! docker pull --platform "$platform" "$img" >/dev/null 2>&1; then
if docker image inspect "$img" >/dev/null 2>&1; then
warn "Using local image for $img (pull failed for $platform)"
else
die "Failed to pull image for $img ($platform)"
fi
fi
docker tag "$img" "$platform_tag" >/dev/null 2>&1 || true
log "Saving $img ($platform) to $tar_path"
if ! docker save -o "$tar_path" "$platform_tag" >/dev/null 2>&1; then
die "Failed to save image $img ($platform) to $tar_path"
fi
}
load_image_for_platform() {
local img="$1"
local platform="$2"
local safe_name
local tar_path
local platform_tag
safe_name="$(image_safe_name "$img")"
tar_path="${DOCKER_IMPORT_DIR}/${safe_name}.${platform//\//-}.tar"
platform_tag="$(image_platform_tag "$img" "$platform")"
if [[ ! -f "$tar_path" ]]; then
ensure_image_artifact "$img" "$platform"
fi
log "Loading $img ($platform) from $tar_path"
docker load -i "$tar_path" 2>&1 | sed 's/^/ /'
docker tag "$platform_tag" "$img"
}
# Returns 0 if the image is already present in k3d's containerd store, 1 otherwise.
image_in_k3d() {
local img="$1"
local cluster="${K3D_CLUSTER_NAME:-}"
local node="k3d-${cluster}-server-0"
if ! docker inspect "$node" >/dev/null 2>&1; then
return 1
fi
docker exec "$node" crictl images --no-trunc -o json 2>/dev/null \
| python3 - "$img" <<'PY'
import json, sys
target = sys.argv[1]
try:
data = json.load(sys.stdin)
except Exception:
sys.exit(1)
for entry in data.get('images', []):
for tag in entry.get('repoTags', []):
if tag == target:
sys.exit(0)
sys.exit(1)
PY
}
# Build the list of images already present in k3d's containerd store.
list_k3d_images() {
local cluster="${K3D_CLUSTER_NAME:-}"
local node="k3d-${cluster}-server-0"
if ! docker inspect "$node" >/dev/null 2>&1; then
return 0
fi
docker exec "$node" crictl images --no-trunc -o json 2>/dev/null \
| python3 - <<'PY'
import json, sys
try:
data = json.load(sys.stdin)
except Exception:
sys.exit(0)
for entry in data.get('images', []):
for tag in entry.get('repoTags', []):
print(tag)
PY
}
write_artifact_manifest() {
local images="$1"
local list_path="${DOCKER_IMPORT_DIR}/gitea-images.txt"
mkdir -p "$DOCKER_IMPORT_DIR"
printf "%s\n" $images > "$list_path"
local platform
for platform in $GITEA_IMAGE_PLATFORMS; do
local platform_list="${DOCKER_IMPORT_DIR}/gitea-images-${platform//\//-}.txt"
: > "$platform_list"
for img in $images; do
local safe_name
safe_name="$(image_safe_name "$img")"
echo "${DOCKER_IMPORT_DIR}/${safe_name}.${platform//\//-}.tar" >> "$platform_list"
done
done
}
prefetch_k3d_images() {
require_cmd docker
docker info >/dev/null 2>&1 || die "Docker daemon is not running"
# Gitea core images (from the helm chart / compose)
local images
images="gitea/gitea:${GITEA_IMAGE_TAG}"
# If a docker-compose.yml exists in the gitea repo, prefer its image list
if [[ -f "$COMPOSE_FILE" ]]; then
local compose
compose="$(compose_cmd)"
ensure_env_file
local files
files=($(compose_files))
local discovered
discovered=$(cd "$DOCKER_DIR" && $compose "${files[@]}" config --images 2>/dev/null | awk 'NF' | sort -u || true)
if [[ -n "$discovered" ]]; then
images="$discovered"
fi
fi
log "Gitea images discovered:"
printf " - %s\n" $images
write_artifact_manifest "$images"
log "Image artifact list written to $DOCKER_IMPORT_DIR/gitea-images.txt"
local platform
for platform in $GITEA_IMAGE_PLATFORMS; do
log "Ensuring artifacts for platform '$platform' in $DOCKER_IMPORT_DIR"
for img in $images; do
ensure_image_artifact "$img" "$platform"
done
done
local deploy_platform
deploy_platform="$(normalize_platform "${GITEA_IMAGE_PLATFORM:-}")"
log "Loading images for platform '$deploy_platform'"
for img in $images; do
load_image_for_platform "$img" "$deploy_platform"
done
# Build a list of images already present in k3d to avoid re-importing them.
log "Checking k3d registry for already-imported images..."
local k3d_existing
k3d_existing="$(list_k3d_images || true)"
local -a import_delta=()
local -a skipped=()
for img in $images; do
if printf '%s\n' "$k3d_existing" | grep -qxF "$img"; then
skipped+=("$img")
else
import_delta+=("$img")
fi
done
if [[ ${#skipped[@]} -gt 0 ]]; then
log "Already in k3d registry (skipping ${#skipped[@]} image(s)):"
printf " [skip] %s\n" "${skipped[@]}"
fi
if [[ ${#import_delta[@]} -eq 0 ]]; then
log "All images already present in k3d registry; nothing to import."
return 0
fi
log "Importing ${#import_delta[@]} image(s) into k3d cluster '${K3D_CLUSTER_NAME:-}' (delta only)..."
local idx=0
for img in "${import_delta[@]}"; do
idx=$(( idx + 1 ))
log " [${idx}/${#import_delta[@]}] Importing $img into k3d..."
if [[ -n "${K3D_CLUSTER_NAME:-}" ]]; then
k3d image import "$img" -c "$K3D_CLUSTER_NAME"
else
k3d image import "$img"
fi
log " [${idx}/${#import_delta[@]}] Done: $img"
done
log "k3d image import complete (${#import_delta[@]} imported, ${#skipped[@]} already present)."
}
resolve_knoe_db_namespace() {
if [[ -n "${KNOE_DB_NAMESPACE:-}" ]]; then
return 0
fi
# Prefer DATABASE_NAMESPACE from config (k3s.cfg sets this to knoe-db)
if [[ -n "${DATABASE_NAMESPACE:-}" ]]; then
KNOE_DB_NAMESPACE="$DATABASE_NAMESPACE"
return 0
fi
if [[ -n "${NAMESPACE:-}" ]]; then
KNOE_DB_NAMESPACE="$NAMESPACE"
return 0
fi
# Probe known namespace patterns before falling back to 'default'
local _ns
for _ns in knoe-db knoe; do
if kubectl get namespace "$_ns" >/dev/null 2>&1; then
KNOE_DB_NAMESPACE="$_ns"
return 0
fi
done
KNOE_DB_NAMESPACE="default"
}
setup_knoe_db_for_gitea() {
log "Ensuring Gitea database and role exist in knoe-db..."
local ns="${KNOE_DB_NAMESPACE:-knoe-db-a0001}"
local primary
primary=$(kubectl -n "$ns" get pods \
-l "cnpg.io/cluster=knoe-db,cnpg.io/instanceRole=primary" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -z "$primary" ]]; then
primary=$(kubectl -n "$ns" get pods -l "cnpg.io/cluster=knoe-db" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
fi
if [[ -z "$primary" ]]; then
warn "No knoe-db pod found in namespace '$ns'; skipping Gitea DB setup."
return 0
fi
local pg_password=""
if kubectl get secret knoe-db-superuser -n "$ns" >/dev/null 2>&1; then
pg_password=$(kubectl get secret knoe-db-superuser -n "$ns" \
-o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null || true)
fi
if [[ -z "$pg_password" ]]; then
pg_password="${POSTGRES_PASSWORD:-}"
fi
if [[ -z "$pg_password" ]]; then
warn "Cannot resolve postgres password; skipping Gitea DB role setup."
return 0
fi
local gitea_db_password="${GITEA_DB_PASSWORD:-${pg_password}}"
kubectl -n "$ns" exec "$primary" -c postgres -- psql -U postgres -d postgres -c "
DO \$\$ BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='gitea') THEN
CREATE ROLE gitea LOGIN PASSWORD '${gitea_db_password}';
END IF;
END \$\$;
" 2>&1 || warn "Could not create gitea role."
local db_exists
db_exists=$(kubectl -n "$ns" exec "$primary" -c postgres -- psql -U postgres -d postgres -tAc \
"SELECT 1 FROM pg_database WHERE datname = 'gitea';" 2>/dev/null || true)
if [[ "$db_exists" != "1" ]]; then
kubectl -n "$ns" exec "$primary" -c postgres -- psql -U postgres -d postgres -c \
"CREATE DATABASE gitea OWNER gitea;" 2>&1 || warn "Could not create gitea database."
fi
log " Gitea database and role ready in knoe-db."
}
resolve_authority_ldap() {
# Resolve the LDAP bind password from the authority pod/secret if not already set
if [[ -n "${AUTHORITY_BIND_PASSWORD:-}" ]]; then
return 0
fi
local ns="${AUTHORITY_NAMESPACE}"
if kubectl get secret authority-ldap-bind -n "$ns" >/dev/null 2>&1; then
AUTHORITY_BIND_PASSWORD=$(kubectl get secret authority-ldap-bind -n "$ns" \
-o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null || true)
fi
if [[ -z "${AUTHORITY_BIND_PASSWORD:-}" ]]; then
warn "Could not resolve LDAP bind password from authority-ldap-bind secret; LDAP config may be incomplete."
fi
}
# Build the Helm values for Gitea, incorporating cloudnative-pg and authority LDAP.
build_helm_values() {
local values_file="$1"
local pg_password="${2:-}"
local gitea_db_password="${GITEA_DB_PASSWORD:-${pg_password}}"
local db_ns="${KNOE_DB_NAMESPACE:-knoe-db-a0001}"
resolve_authority_ldap
local ldap_host
ldap_host="${AUTHORITY_SERVICE}.${AUTHORITY_NAMESPACE}.svc.cluster.local"
local ingress_enabled="false"
local ingress_class="${GITEA_INGRESS_CLASS:-kong}"
if [[ "$MODE" == "k3s" || "$MODE" == "k8s" ]]; then
ingress_enabled="true"
fi
cat > "$values_file" <<EOF
image:
registry: docker.io
repository: gitea/gitea
tag: "${GITEA_IMAGE_TAG}"
rootless: false
pullPolicy: IfNotPresent
service:
http:
type: ClusterIP
port: 3000
ssh:
type: ClusterIP
port: 22
ingress:
enabled: ${ingress_enabled}
className: ${ingress_class}
annotations:
konghq.com/strip-path: "false"
hosts:
- host: ${GITEA_DOMAIN}
paths:
- path: /
pathType: Prefix
tls: []
gitea:
config:
server:
DOMAIN: ${GITEA_DOMAIN}
SSH_DOMAIN: ${GITEA_SSH_DOMAIN}
ROOT_URL: "http://${GITEA_DOMAIN}/"
HTTP_PORT: 3000
SSH_PORT: 22
database:
DB_TYPE: postgres
HOST: "${KNOE_DB_SERVICE}.${db_ns}.svc.cluster.local:5432"
NAME: gitea
USER: gitea
PASSWD: "${gitea_db_password}"
SSL_MODE: disable
service:
ENABLE_REVERSE_PROXY_AUTHENTICATION: "true"
ENABLE_REVERSE_PROXY_AUTO_REGISTRATION: "true"
REVERSE_PROXY_TRUSTED_PROXIES: "10.42.0.0/16"
session:
PROVIDER: db
cache:
ADAPTER: memory
queue:
TYPE: level
indexer:
ISSUE_INDEXER_TYPE: bleve
REPO_INDEXER_ENABLED: "true"
ldap:
- name: "knoe-authority"
securityProtocol: unencrypted
host: "${ldap_host}"
port: "${AUTHORITY_LDAP_PORT}"
userSearchBase: "ou=users,${AUTHORITY_BASE_DN}"
userFilter: "(&(objectClass=inetOrgPerson)(uid=%s))"
adminFilter: "(memberOf=cn=admins,ou=groups,${AUTHORITY_BASE_DN})"
emailAttribute: mail
usernameAttribute: uid
firstnameAttribute: givenName
surnameAttribute: sn
publicSSHKeyAttribute: sshPublicKey
bindDn: "${AUTHORITY_BIND_DN}"
bindPassword: "${AUTHORITY_BIND_PASSWORD:-}"
synchronizeUsers: "true"
attributesInBind: "false"
# Disable the bundled PostgreSQL and valkey/redis clusters — we use knoe-db + memory cache instead.
postgresql:
enabled: false
postgresql-ha:
enabled: false
valkey-cluster:
enabled: false
redis-cluster:
enabled: false
persistence:
enabled: true
size: 10Gi
storageClass: "${GITEA_STORAGE_CLASS:-gitea-local-d005}"
EOF
}
ensure_gitea_namespace() {
if ! kubectl get namespace "$GITEA_NAMESPACE" >/dev/null 2>&1; then
log "Creating '$GITEA_NAMESPACE' namespace..."
kubectl create namespace "$GITEA_NAMESPACE"
fi
}
force_reset_gitea_namespace() {
if ! kubectl get namespace "$GITEA_NAMESPACE" >/dev/null 2>&1; then
ensure_gitea_namespace
return
fi
log "Resetting '$GITEA_NAMESPACE' namespace..."
kubectl delete all --all -n "$GITEA_NAMESPACE" --timeout=60s >/dev/null 2>&1 || true
kubectl delete namespace "$GITEA_NAMESPACE" --ignore-not-found --wait=false >/dev/null 2>&1 || true
if kubectl get namespace "$GITEA_NAMESPACE" >/dev/null 2>&1; then
if ! kubectl wait --for=delete "namespace/$GITEA_NAMESPACE" --timeout=120s >/dev/null 2>&1; then
warn "Namespace deletion stalled; clearing finalizers"
python3 - <<PY | kubectl replace --raw "/api/v1/namespaces/${GITEA_NAMESPACE}/finalize" -f - >/dev/null 2>&1 || true
import json
import subprocess
import sys
try:
raw = subprocess.check_output(["kubectl", "get", "namespace", "${GITEA_NAMESPACE}", "-o", "json"])
if not raw:
sys.exit(0)
data = json.loads(raw)
if "spec" in data:
data["spec"]["finalizers"] = []
print(json.dumps(data))
except Exception:
sys.exit(0)
PY
kubectl wait --for=delete "namespace/$GITEA_NAMESPACE" --timeout=60s >/dev/null 2>&1 || true
fi
fi
ensure_gitea_namespace
}
write_kong_ingress_k3s() {
local out_dir="$1"
cat > "$out_dir/gitea-kong-ingress.yaml" <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gitea-kong
namespace: ${GITEA_NAMESPACE}
annotations:
konghq.com/strip-path: "false"
konghq.com/protocols: "http,https"
spec:
ingressClassName: kong
rules:
- host: ${GITEA_DOMAIN}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gitea-http
port:
number: 3000
EOF
log " Kong ingress manifest written: $out_dir/gitea-kong-ingress.yaml"
}
run_helm() {
if [[ "$MODE" == "local" ]]; then
return 1
fi
ensure_helm
require_cmd kubectl
kubectl cluster-info >/dev/null 2>&1 || return 1
if [[ "$FORCE" == "true" ]]; then
kubectl delete namespace "$GITEA_NAMESPACE" --ignore-not-found --wait=false >/dev/null 2>&1 || true
fi
resolve_knoe_db_namespace
setup_knoe_db_for_gitea
# Resolve postgres password for DB values
local pg_password=""
local db_ns="${KNOE_DB_NAMESPACE:-knoe-db-a0001}"
if kubectl get secret knoe-db-superuser -n "$db_ns" >/dev/null 2>&1; then
pg_password=$(kubectl get secret knoe-db-superuser -n "$db_ns" \
-o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null || true)
fi
if [[ -z "$pg_password" ]]; then
pg_password="${POSTGRES_PASSWORD:-}"
fi
local values_file
values_file="$(mktemp /tmp/gitea-values.XXXXXX.yaml)"
build_helm_values "$values_file" "$pg_password"
# Ensure the helm-gitea chart repo is registered
if ! helm repo list 2>/dev/null | grep -q "gitea-charts"; then
log "Adding gitea-charts Helm repo..."
helm repo add gitea-charts https://dl.gitea.com/charts/ >/dev/null 2>&1 || warn "helm repo add failed"
fi
helm repo update >/dev/null 2>&1 || true
local k8s_dir="$SCRIPT_DIR/k8s"
mkdir -p "$k8s_dir"
if [[ "$HELM_TEMPLATE_ONLY" == "true" ]]; then
log "Rendering Gitea Helm manifests to $k8s_dir (template only)..."
helm template "$HELM_RELEASE" gitea-charts/gitea \
--namespace "$GITEA_NAMESPACE" \
-f "$values_file" \
--output-dir "$k8s_dir" >/dev/null
if [[ "$MODE" == "k3s" ]]; then
write_kong_ingress_k3s "$k8s_dir"
fi
log "Helm template only; manifests ready in gitea/k8s"
rm -f "$values_file"
return 0
fi
log "Installing Gitea via Helm into namespace '$GITEA_NAMESPACE'..."
if helm upgrade --install "$HELM_RELEASE" gitea-charts/gitea \
-n "$GITEA_NAMESPACE" --create-namespace -f "$values_file"; then
rm -f "$values_file"
# Apply Kong ingress separately for k3s context
if [[ "$MODE" == "k3s" ]]; then
write_kong_ingress_k3s "$k8s_dir"
log "Applying Kong ingress for git.knoe.org..."
kubectl apply -f "$k8s_dir/gitea-kong-ingress.yaml"
fi
register_gitea_ports
log "Deployment complete via Helm."
return 0
else
rm -f "$values_file"
warn "Helm install failed; falling back to manifest deploy."
return 1
fi
}
run_manifest_fallback() {
local manifest_file
manifest_file="$(mktemp /tmp/gitea-manifest.XXXXXX.yaml)"
cat > "$manifest_file" <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitea
namespace: ${GITEA_NAMESPACE}
spec:
replicas: 1
selector:
matchLabels:
app: gitea
template:
metadata:
labels:
app: gitea
spec:
containers:
- name: gitea
image: gitea/gitea:${GITEA_IMAGE_TAG}
ports:
- name: http
containerPort: 3000
- name: ssh
containerPort: 22
env:
- name: GITEA__database__DB_TYPE
value: postgres
- name: GITEA__database__HOST
value: "${KNOE_DB_SERVICE}.${KNOE_DB_NAMESPACE:-knoe-db-a0001}.svc.cluster.local:5432"
- name: GITEA__database__NAME
value: gitea
- name: GITEA__database__USER
value: gitea
- name: GITEA__database__PASSWD
value: "${GITEA_DB_PASSWORD:-}"
- name: GITEA__server__DOMAIN
value: "${GITEA_DOMAIN}"
- name: GITEA__server__ROOT_URL
value: "http://${GITEA_DOMAIN}/"
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: gitea-http
namespace: ${GITEA_NAMESPACE}
spec:
selector:
app: gitea
ports:
- name: http
port: 3000
targetPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: gitea-ssh
namespace: ${GITEA_NAMESPACE}
spec:
selector:
app: gitea
ports:
- name: ssh
port: 22
targetPort: 22
EOF
if [[ "$MODE" == "k3s" ]]; then
cat >> "$manifest_file" <<EOF
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gitea-kong
namespace: ${GITEA_NAMESPACE}
annotations:
konghq.com/strip-path: "false"
konghq.com/protocols: "http,https"
spec:
ingressClassName: kong
rules:
- host: ${GITEA_DOMAIN}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gitea-http
port:
number: 3000
EOF
fi
log "Applying fallback manifest..."
kubectl apply -f "$manifest_file"
rm -f "$manifest_file"
}
run_k3d() {
# Helm-first path (preferred)
if run_helm; then
return 0
fi
log "Knoe::Gitea k3d deploy (Kubernetes)"
log "Repo: $GITEA_DIR"
require_cmd kubectl
ensure_k3d
ensure_k3d_cluster
kubectl cluster-info >/dev/null 2>&1 || die "Kubernetes cluster not reachable"
if [[ "$FORCE" == "true" ]]; then
force_reset_gitea_namespace
else
ensure_gitea_namespace
fi
GITEA_IMAGE_PLATFORM="$(normalize_platform "${GITEA_IMAGE_PLATFORM:-}")"
prefetch_k3d_images
resolve_knoe_db_namespace
setup_knoe_db_for_gitea
run_manifest_fallback
register_gitea_ports
log "Deployment complete."
log "Access Gitea via Kong proxy (check ingress/service in '$GITEA_NAMESPACE' namespace)."
}
run_k8s() {
# Helm-first path (preferred) — for k3s use context knoe_k3s
if [[ "$MODE" == "k3s" ]]; then
local ctx="${PROLE_K3S_CONTEXT:-knoe_k3s}"
if kubectl config get-contexts "$ctx" >/dev/null 2>&1; then
log "Switching to k3s context: $ctx"
kubectl config use-context "$ctx" >/dev/null 2>&1 || true
fi
fi
if run_helm; then
return 0
fi
log "Knoe::Gitea k8s/k3s deploy (Kubernetes - generic)"
log "Repo: $GITEA_DIR"
require_cmd kubectl
kubectl cluster-info >/dev/null 2>&1 || die "Kubernetes cluster not reachable"
if [[ "$FORCE" == "true" ]]; then
force_reset_gitea_namespace
else
ensure_gitea_namespace
fi
resolve_knoe_db_namespace
setup_knoe_db_for_gitea
run_manifest_fallback
register_gitea_ports
log "Deployment complete."
log "Access Gitea at http://${GITEA_DOMAIN}/ (via Kong ingress)"
}
main() {
parse_args "$@"
load_knoe_cfg
apply_defaults
if [[ -z "$MODE" ]]; then
usage
exit 1
fi
log "Starting Gitea deployment..."
log "Mode: $MODE"
case "$MODE" in
local)
run_local
;;
k3d)
run_k3d
;;
k8s|k3s)
run_k8s
;;
*)
die "Unsupported mode: $MODE"
;;
esac
}
main "$@"