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) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,19 @@
|
|||||||
|
|
||||||
All notable changes to the operator helper scripts. Newest on top.
|
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
|
## 2026-06-17
|
||||||
|
|
||||||
### Added — `gitea-image-prune.sh`
|
### Added — `gitea-image-prune.sh`
|
||||||
|
|||||||
@@ -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
|
> accumulated orphan manifests on `felhom-hub` then freed **86 MiB**. After
|
||||||
> reclaim, surviving tags (`latest`, `0.1.3`, …) still `docker pull` cleanly.
|
> 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 <path>`
|
||||||
|
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 <path>`. The
|
Pass an admin-user token via `GITEA_TOKEN` (env) or `--token-file <path>`. The
|
||||||
token must belong to a Gitea **site-admin** user. Minimal fine-grained scopes
|
token must belong to a Gitea **site-admin** user. Minimal fine-grained scopes
|
||||||
|
|||||||
+53
-7
@@ -42,6 +42,12 @@
|
|||||||
# and their blobs are freed by the daily "@midnight" run (or on the next Gitea
|
# 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).
|
# 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
|
# REQUIRED TOKEN (env GITEA_TOKEN, or --token-file). Must belong to a Gitea
|
||||||
# site-admin user. Minimal fine-grained scopes (Gitea 1.26):
|
# site-admin user. Minimal fine-grained scopes (Gitea 1.26):
|
||||||
# read:package — list packages / versions / files (list, prune planning)
|
# read:package — list packages / versions / files (list, prune planning)
|
||||||
@@ -149,16 +155,55 @@ for bin in curl jq; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
# Token: --token-file beats env. Never printed.
|
# 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
|
if [[ -n "$TOKEN_FILE" ]]; then
|
||||||
[[ -r "$TOKEN_FILE" ]] || { error "--token-file not readable: $TOKEN_FILE"; exit 1; }
|
[[ -r "$TOKEN_FILE" ]] || { error "--token-file not readable: $TOKEN_FILE"; exit 1; }
|
||||||
GITEA_TOKEN="$(tr -d ' \t\r\n' < "$TOKEN_FILE")"
|
GITEA_TOKEN="$(tr -d ' \t\r\n' < "$TOKEN_FILE")"
|
||||||
|
CRED_SRC="--token-file"
|
||||||
fi
|
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
|
if [[ -z "$GITEA_TOKEN" ]]; then
|
||||||
error "No token. Set GITEA_TOKEN env or pass --token-file <path>."
|
discover_git_credential || true
|
||||||
error "Needs a site-admin token with read/write:package + read/write:admin."
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$GITEA_TOKEN" ]]; then
|
||||||
|
error "No credentials. Set GITEA_TOKEN env, pass --token-file <path>, 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
|
exit 1
|
||||||
fi
|
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
|
# Default audit log
|
||||||
if [[ -z "$LOG_FILE" ]]; then
|
if [[ -z "$LOG_FILE" ]]; then
|
||||||
LOG_FILE="${SCRIPT_DIR}/logs/gitea-prune-$(date +%Y-%m-%d).log"
|
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() {
|
api_get() {
|
||||||
local path="$1" out code
|
local path="$1" out code
|
||||||
out="$(mktemp)"
|
out="$(mktemp)"
|
||||||
code="$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
code="$(curl -sS "${AUTH[@]}" \
|
||||||
-o "$out" -w '%{http_code}' "${GITEA_URL}${path}" || true)"
|
-o "$out" -w '%{http_code}' "${GITEA_URL}${path}" || true)"
|
||||||
if [[ ! "$code" =~ ^2[0-9][0-9]$ ]]; then
|
if [[ ! "$code" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
error "GET ${path} -> HTTP ${code}"
|
error "GET ${path} -> HTTP ${code}"
|
||||||
@@ -194,7 +239,7 @@ api_get() {
|
|||||||
|
|
||||||
api_delete() {
|
api_delete() {
|
||||||
local path="$1" code
|
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)"
|
-o "$TMP/del.body" -w '%{http_code}' "${GITEA_URL}${path}" || true)"
|
||||||
echo "$code"
|
echo "$code"
|
||||||
[[ "$code" == "204" ]]
|
[[ "$code" == "204" ]]
|
||||||
@@ -202,7 +247,7 @@ api_delete() {
|
|||||||
|
|
||||||
api_post() {
|
api_post() {
|
||||||
local path="$1" code
|
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)"
|
-o "$TMP/post.body" -w '%{http_code}' "${GITEA_URL}${path}" || true)"
|
||||||
if [[ ! "$code" =~ ^2[0-9][0-9]$ ]]; then
|
if [[ ! "$code" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
error "POST ${path} -> HTTP ${code}"
|
error "POST ${path} -> HTTP ${code}"
|
||||||
@@ -215,7 +260,7 @@ api_post() {
|
|||||||
oci_get() {
|
oci_get() {
|
||||||
local name="$1" ref="$2" out code
|
local name="$1" ref="$2" out code
|
||||||
out="$(mktemp)"
|
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' \
|
-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)"
|
-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
|
if [[ ! "$code" =~ ^2[0-9][0-9]$ ]]; then rm -f "$out"; return 1; fi
|
||||||
@@ -348,7 +393,7 @@ find_orphans() {
|
|||||||
# echoes "ok <code>" on 2xx, "denied" on 403, "err <code>" otherwise; rc reflects.
|
# echoes "ok <code>" on 2xx, "denied" on 403, "err <code>" otherwise; rc reflects.
|
||||||
trigger_cleanup_cron() {
|
trigger_cleanup_cron() {
|
||||||
local code
|
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}' \
|
-o "$TMP/cron.body" -w '%{http_code}' \
|
||||||
"${GITEA_URL}/api/v1/admin/cron/${CLEANUP_CRON}" || true)"
|
"${GITEA_URL}/api/v1/admin/cron/${CLEANUP_CRON}" || true)"
|
||||||
if [[ "$code" =~ ^2[0-9][0-9]$ ]]; then echo "ok ${code}"; return 0; fi
|
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 "║ Gitea container-image prune ║"
|
||||||
info "╚══════════════════════════════════════════╝"
|
info "╚══════════════════════════════════════════╝"
|
||||||
info "Server: ${GITEA_URL} Owner: ${OWNER} Log: ${LOG_FILE}"
|
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)
|
# Sanity: Gitea version (public, no auth)
|
||||||
GV="$(curl -fsS "${GITEA_URL}/api/v1/version" 2>/dev/null | jq -r '.version' 2>/dev/null || echo '?')"
|
GV="$(curl -fsS "${GITEA_URL}/api/v1/version" 2>/dev/null | jq -r '.version' 2>/dev/null || echo '?')"
|
||||||
|
|||||||
Reference in New Issue
Block a user