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>
This commit is contained in:
chrisfu 2026-05-09 18:53:55 -07:00
parent abb38fc1e9
commit dbe249ce9f
2 changed files with 106 additions and 24 deletions

View File

@ -897,6 +897,57 @@ gitea_api_token() {
printf '%s' "$resp" | grep -o '"sha1":"[^"]*"' | cut -d'"' -f4
}
# gitea_helm_admin_token [token_name]
# Reads the Helm bootstrap admin password from the k8s secret created by the
# Gitea Helm chart ({release}-gitea, key admin-password) and exchanges it for
# a REST API token. This works even when the gitea admin CLI is broken (e.g.
# wrong DB hostname in app.ini) because the request goes through the running
# web server which has the correct GITEA__database__HOST env var.
# Prints the token sha1 to stdout; returns 1 on failure.
gitea_helm_admin_token() {
local token_name="${1:-knoe-installer-helm}"
local secret="${GITEA_HELM_RELEASE:-gitea}-gitea"
local admin_user="${GITEA_HELM_ADMIN_USER:-gitea_admin}"
local admin_pw
admin_pw=$(kubectl -n "$GITEA_NAMESPACE" get secret "$secret" \
-o jsonpath='{.data.admin-password}' 2>/dev/null | b64_decode || true)
if [[ -z "$admin_pw" ]]; then
warn " Gitea: Helm admin secret '${secret}' not found or empty in namespace ${GITEA_NAMESPACE}"
return 1
fi
local tok
tok=$(gitea_api_token "$admin_user" "$admin_pw" "$token_name")
if [[ -z "$tok" ]]; then
warn " Gitea: could not obtain API token for Helm admin '${admin_user}'"
return 1
fi
log " Gitea: obtained bootstrap token from Helm admin '${admin_user}'"
printf '%s' "$tok"
}
# gitea_api_set_password <admin_token> <username> <new_password>
# Changes a user's password via the Gitea admin REST API.
# Uses the running web server (not the CLI), so it works even when app.ini
# has a wrong database hostname (the server overrides via GITEA__ env vars).
gitea_api_set_password() {
local token="$1" username="$2" pw="$3"
local http_code
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
-X PATCH \
-H "Authorization: token ${token}" \
-H "Content-Type: application/json" \
-d "{\"login_name\":\"${username}\",\"source_id\":0,\"password\":\"${pw}\"}" \
"https://${GITEA_HOST}/api/v1/admin/users/${username}" 2>/dev/null) || true
if [[ "$http_code" =~ ^2 ]]; then
return 0
else
warn " Gitea: admin API set-password returned HTTP ${http_code} for '${username}'"
return 1
fi
}
promote_gitea_admin() {
local token="$GITEA_ADMIN_TOKEN"
@ -906,27 +957,47 @@ promote_gitea_admin() {
-o jsonpath='{.data.token}' 2>/dev/null | b64_decode || true)
fi
# 2. Obtain token via 1Password-backed password for KNOE_ADMIN_PRINCIPAL.
# gitea_ensure_password idempotently creates the 1Password item if needed,
# then sets the password in Gitea and exchanges it for a short-lived token.
# The plaintext password is held only in a local variable and cleared below.
# 2. Bootstrap via the Helm admin account (k8s secret → REST API token).
# This path works even when the gitea admin CLI is broken (app.ini has wrong
# DB hostname) because REST calls go through the running server which has the
# correct GITEA__database__HOST env var. Once we have a bootstrap token, use
# it to also set KNOE_ADMIN_PRINCIPAL's password via API (from 1Password).
local _helm_token=""
if [[ -z "$token" ]]; then
local _pw=""
if _pw=$(gitea_ensure_password "${KNOE_ADMIN_PRINCIPAL}"); then
log "Gitea: setting password for '${KNOE_ADMIN_PRINCIPAL}' from 1Password ..."
if gitea_set_password "${KNOE_ADMIN_PRINCIPAL}" "$_pw"; then
log "Gitea: password set; requesting API token ..."
token=$(gitea_api_token "${KNOE_ADMIN_PRINCIPAL}" "$_pw" "knoe-installer")
[[ -n "$token" ]] && log "Gitea: API token obtained for '${KNOE_ADMIN_PRINCIPAL}'"
else
warn "Gitea: failed to set password for '${KNOE_ADMIN_PRINCIPAL}'"
if _helm_token=$(gitea_helm_admin_token "knoe-installer-helm"); then
token="$_helm_token"
# Opportunistically set KNOE_ADMIN_PRINCIPAL's 1Password-backed password
# via REST so chrisfu can log in as fallback if SPNEGO is unavailable.
if command -v op >/dev/null 2>&1; then
local _pw=""
if _pw=$(gitea_ensure_password "${KNOE_ADMIN_PRINCIPAL}"); then
log "Gitea: setting password for '${KNOE_ADMIN_PRINCIPAL}' via admin REST API ..."
gitea_api_set_password "$_helm_token" "${KNOE_ADMIN_PRINCIPAL}" "$_pw" \
&& log "Gitea: password set for '${KNOE_ADMIN_PRINCIPAL}'" \
|| true
_pw=""
fi
fi
_pw="" # clear plaintext from memory immediately
fi
fi
# 3. Fall back to Helm bootstrap admin via kubectl exec generate-access-token.
# Useful when KNOE_ADMIN_PRINCIPAL has not yet logged in and op is unavailable.
# 3. Try 1Password → set password via CLI → exchange for token.
# Kept as fallback for when the Helm admin secret is absent (e.g. non-Helm deploy).
if [[ -z "$token" ]]; then
local _pw=""
if _pw=$(gitea_ensure_password "${KNOE_ADMIN_PRINCIPAL}"); then
log "Gitea: setting password for '${KNOE_ADMIN_PRINCIPAL}' via kubectl exec ..."
if gitea_set_password "${KNOE_ADMIN_PRINCIPAL}" "$_pw"; then
token=$(gitea_api_token "${KNOE_ADMIN_PRINCIPAL}" "$_pw" "knoe-installer")
[[ -n "$token" ]] && log "Gitea: API token obtained for '${KNOE_ADMIN_PRINCIPAL}'"
else
warn "Gitea: CLI password change failed (DB host mismatch in app.ini?)"
fi
_pw=""
fi
fi
# 4. Last resort: kubectl exec generate-access-token (CLI path, may fail if DB broken).
if [[ -z "$token" ]]; then
local _gitea_pod
_gitea_pod=$(kubectl -n "$GITEA_NAMESPACE" get pods \
@ -934,12 +1005,12 @@ promote_gitea_admin() {
--field-selector=status.phase=Running \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -n "$_gitea_pod" ]]; then
log "Gitea: attempting token generation via kubectl exec (Helm bootstrap admin) ..."
log "Gitea: attempting token generation via kubectl exec (last resort) ..."
local _u
for _u in "gitea_admin" "${KNOE_ADMIN_PRINCIPAL}"; do
token=$(kubectl -n "$GITEA_NAMESPACE" exec "$_gitea_pod" -c gitea -- \
su git -s /bin/bash -c \
"gitea admin user generate-access-token --username ${_u} --token-name knoe-installer --scopes 'write:admin,read:user' --raw 2>/dev/null" \
"gitea admin user generate-access-token --username ${_u} --token-name knoe-installer --scopes write:admin,read:user --raw 2>/dev/null" \
2>/dev/null | tail -1 || true)
[[ -n "$token" ]] && { log "Gitea: token generated for user '${_u}'"; break; }
done
@ -948,7 +1019,7 @@ promote_gitea_admin() {
if [[ -z "$token" ]]; then
warn "No Gitea admin token available — skipping admin promotion"
warn " Ensure 'op signin' is active on this host, then re-run."
warn " Ensure 'op signin' is active and the Helm admin secret exists, then re-run."
warn " Or set GITEA_ADMIN_TOKEN and re-run."
return 0
fi

View File

@ -623,16 +623,27 @@ resolve_knoe_db_namespace() {
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
if kubectl get namespace knoe >/dev/null 2>&1; then
KNOE_DB_NAMESPACE="knoe"
else
KNOE_DB_NAMESPACE="default"
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() {