From 5b4d8ec6cf85ce65e71b493bd56b32922824a9a4 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Wed, 17 Jun 2026 09:28:55 +0200 Subject: [PATCH] gitea-image-prune.sh: auto-discover credentials from git When GITEA_TOKEN/--token-file aren't set and the script runs inside a Gitea-host clone, reuse git's stored credential: the token embedded in the remote URL, else a configured credential helper (git credential fill, no prompting). Switch to HTTP Basic auth (user:token) when a username is known so both API tokens and the embedded-URL/helper credential work; keep the token header for a bare GITEA_TOKEN. Banner reports the source. Live-verified: env token + git credential helper (as kisfenyo) both list and resolve OCI sizes. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 13 ++++++++++ README.md | 18 ++++++++++++- gitea-image-prune.sh | 60 ++++++++++++++++++++++++++++++++++++++------ 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14ce0c9..39190ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to the operator helper scripts. Newest on top. +## 2026-06-17 (later) + +### Changed — `gitea-image-prune.sh` +- Credential auto-discovery: when `GITEA_TOKEN`/`--token-file` are not set and the + script runs inside a Gitea-host clone, it reuses git's stored credential — the + token embedded in the remote URL, else a configured credential helper + (`git credential fill`, never prompting). Lets you run it from a configured + clone with no token. Startup banner reports the credential source + user. +- Auth now uses HTTP Basic (`user:token`) when a username is known (so both API + tokens and the embedded-URL/helper credential work), falling back to the + `Authorization: token` header for a bare `GITEA_TOKEN`. Live-verified both paths + (env token; git credential helper as `kisfenyo`). + ## 2026-06-17 ### Added — `gitea-image-prune.sh` diff --git a/README.md b/README.md index 0699d68..c3eedb6 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,23 @@ Confirmed live on Gitea 1.26.2 (2026-06-17), the reclaim path is **three steps** > accumulated orphan manifests on `felhom-hub` then freed **86 MiB**. After > reclaim, surviving tags (`latest`, `0.1.3`, …) still `docker pull` cleanly. -### Token & required scopes +### Credentials + +The script resolves credentials in this order: + +1. `GITEA_TOKEN` env var +2. `--token-file ` +3. **git's stored credential for the Gitea host** — auto-discovered when you run + the script inside a clone: first the token embedded in the remote URL + (`https://user:token@host/…`), else a configured **credential helper** + (`git credential fill`). No prompting. + +So from a configured clone you can just run `./gitea-image-prune.sh --repo … list` +with no token at all. The startup banner prints which source was used. Caveat: a +*git* credential may only carry repo scope — if it lacks package/admin scope, the +call returns a 403 naming the missing scope (see below). + +### Required scopes Pass an admin-user token via `GITEA_TOKEN` (env) or `--token-file `. The token must belong to a Gitea **site-admin** user. Minimal fine-grained scopes diff --git a/gitea-image-prune.sh b/gitea-image-prune.sh index 72fea44..3061eca 100755 --- a/gitea-image-prune.sh +++ b/gitea-image-prune.sh @@ -42,6 +42,12 @@ # and their blobs are freed by the daily "@midnight" run (or on the next Gitea # restart — RUN_AT_START was enabled in app.ini on 2026-06-17). # +# CREDENTIALS (in order: GITEA_TOKEN env -> --token-file -> git's stored +# credential for the Gitea host: the remote URL's embedded token, else a +# configured credential helper). Auto-discovery lets you just run the script +# inside a clone with no token fuss — but a *git* credential may only have repo +# scope; if so, a 403 will name the missing package/admin scope. +# # REQUIRED TOKEN (env GITEA_TOKEN, or --token-file). Must belong to a Gitea # site-admin user. Minimal fine-grained scopes (Gitea 1.26): # read:package — list packages / versions / files (list, prune planning) @@ -149,16 +155,55 @@ for bin in curl jq; do done # Token: --token-file beats env. Never printed. +GITEA_USER="${GITEA_USER:-}" # set when credentials come with a username (-> Basic auth) +CRED_SRC="GITEA_TOKEN env" + if [[ -n "$TOKEN_FILE" ]]; then [[ -r "$TOKEN_FILE" ]] || { error "--token-file not readable: $TOKEN_FILE"; exit 1; } GITEA_TOKEN="$(tr -d ' \t\r\n' < "$TOKEN_FILE")" + CRED_SRC="--token-file" fi + +# Auto-discover from git when no explicit token: reuse the credential git already +# has for the Gitea host (embedded remote URL, or a configured credential helper). +# Convenient, but a *git* token may lack package/admin scopes — a 403 will say so. +discover_git_credential() { + command -v git &>/dev/null || return 1 + git rev-parse --is-inside-work-tree &>/dev/null || return 1 + local host="${GITEA_URL#*://}"; host="${host%%/*}" + # (1) credentials embedded in the remote URL (https://user:token@host/...) + local url; url="$(git remote get-url origin 2>/dev/null || true)" + if [[ "$url" == *"$host"* && "$url" =~ ^https?://([^:/@]+):([^@/]+)@ ]]; then + GITEA_USER="${BASH_REMATCH[1]}"; GITEA_TOKEN="${BASH_REMATCH[2]}"; CRED_SRC="git remote URL"; return 0 + fi + # (2) a configured credential helper (store / cache / manager). Never prompt. + if git config --get credential.helper &>/dev/null; then + local out user pass + out="$(printf 'protocol=https\nhost=%s\n\n' "$host" | GIT_TERMINAL_PROMPT=0 git credential fill 2>/dev/null || true)" + pass="$(printf '%s\n' "$out" | sed -n 's/^password=//p' | head -1)" + user="$(printf '%s\n' "$out" | sed -n 's/^username=//p' | head -1)" + if [[ -n "$pass" ]]; then GITEA_TOKEN="$pass"; GITEA_USER="${user:-$OWNER}"; CRED_SRC="git credential helper"; return 0; fi + fi + return 1 +} + if [[ -z "$GITEA_TOKEN" ]]; then - error "No token. Set GITEA_TOKEN env or pass --token-file ." - error "Needs a site-admin token with read/write:package + read/write:admin." + discover_git_credential || true +fi + +if [[ -z "$GITEA_TOKEN" ]]; then + error "No credentials. Set GITEA_TOKEN env, pass --token-file , or run inside" + error "a clone of a ${GITEA_URL#*://} repo whose git credentials are configured." + error "Needs a site-admin token with read/write:package + read/write:admin scope." exit 1 fi +# Auth method: Basic (user:token) when a username is known — works for both API +# tokens and passwords; bare 'token' header otherwise. OCI /v2/ always uses Basic. +declare -a AUTH +if [[ -n "$GITEA_USER" ]]; then AUTH=(-u "${GITEA_USER}:${GITEA_TOKEN}"); else AUTH=(-H "Authorization: token ${GITEA_TOKEN}"); fi +OCI_USER="${GITEA_USER:-$OWNER}" + # Default audit log if [[ -z "$LOG_FILE" ]]; then LOG_FILE="${SCRIPT_DIR}/logs/gitea-prune-$(date +%Y-%m-%d).log" @@ -182,7 +227,7 @@ audit() { # free-form line -> audit log (token-free by construction) api_get() { local path="$1" out code out="$(mktemp)" - code="$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \ + code="$(curl -sS "${AUTH[@]}" \ -o "$out" -w '%{http_code}' "${GITEA_URL}${path}" || true)" if [[ ! "$code" =~ ^2[0-9][0-9]$ ]]; then error "GET ${path} -> HTTP ${code}" @@ -194,7 +239,7 @@ api_get() { api_delete() { local path="$1" code - code="$(curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \ + code="$(curl -sS -X DELETE "${AUTH[@]}" \ -o "$TMP/del.body" -w '%{http_code}' "${GITEA_URL}${path}" || true)" echo "$code" [[ "$code" == "204" ]] @@ -202,7 +247,7 @@ api_delete() { api_post() { local path="$1" code - code="$(curl -sS -X POST -H "Authorization: token ${GITEA_TOKEN}" \ + code="$(curl -sS -X POST "${AUTH[@]}" \ -o "$TMP/post.body" -w '%{http_code}' "${GITEA_URL}${path}" || true)" if [[ ! "$code" =~ ^2[0-9][0-9]$ ]]; then error "POST ${path} -> HTTP ${code}" @@ -215,7 +260,7 @@ api_post() { oci_get() { local name="$1" ref="$2" out code out="$(mktemp)" - code="$(curl -sS -u "${OWNER}:${GITEA_TOKEN}" \ + code="$(curl -sS -u "${OCI_USER}:${GITEA_TOKEN}" \ -H 'Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json' \ -o "$out" -w '%{http_code}' "${GITEA_URL}/v2/${OWNER}/${name}/manifests/${ref}" || true)" if [[ ! "$code" =~ ^2[0-9][0-9]$ ]]; then rm -f "$out"; return 1; fi @@ -348,7 +393,7 @@ find_orphans() { # echoes "ok " on 2xx, "denied" on 403, "err " otherwise; rc reflects. trigger_cleanup_cron() { local code - code="$(curl -sS -X POST -H "Authorization: token ${GITEA_TOKEN}" \ + code="$(curl -sS -X POST "${AUTH[@]}" \ -o "$TMP/cron.body" -w '%{http_code}' \ "${GITEA_URL}/api/v1/admin/cron/${CLEANUP_CRON}" || true)" if [[ "$code" =~ ^2[0-9][0-9]$ ]]; then echo "ok ${code}"; return 0; fi @@ -617,6 +662,7 @@ info "╔═══════════════════════ info "║ Gitea container-image prune ║" info "╚══════════════════════════════════════════╝" info "Server: ${GITEA_URL} Owner: ${OWNER} Log: ${LOG_FILE}" +info "Auth: ${CRED_SRC}$( [[ -n "$GITEA_USER" ]] && echo " (user: ${GITEA_USER})")" # Sanity: Gitea version (public, no auth) GV="$(curl -fsS "${GITEA_URL}/api/v1/version" 2>/dev/null | jq -r '.version' 2>/dev/null || echo '?')"