192 lines
4.8 KiB
Bash
Executable File
192 lines
4.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# =============================================================================
|
|
# Defaults (override via CLI flags)
|
|
# =============================================================================
|
|
REGISTRY="https://git.motovaultpro.com"
|
|
USERNAME="egullickson"
|
|
TOKEN=""
|
|
KEEP_TAG="latest"
|
|
DRY_RUN=1
|
|
|
|
REPOS=(
|
|
"egullickson/frontend"
|
|
"egullickson/backend"
|
|
)
|
|
# =============================================================================
|
|
|
|
need() { command -v "$1" >/dev/null || { echo "ERROR: missing dependency: $1"; exit 1; }; }
|
|
need curl
|
|
need jq
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage:
|
|
$0 --token=YOUR_PAT [--user=egullickson] [--registry=https://git.motovaultpro.com]
|
|
[--keep-tag=latest] [--dry-run|--no-dry-run]
|
|
|
|
Examples:
|
|
# Preview
|
|
$0 --token=XXXX --dry-run
|
|
|
|
# Actually delete
|
|
$0 --token=XXXX --no-dry-run
|
|
|
|
Notes:
|
|
- Deletes all tags except KEEP_TAG for repos:
|
|
${REPOS[*]}
|
|
- Protects KEEP_TAG by digest (won't delete anything pointing to KEEP_TAG digest).
|
|
EOF
|
|
}
|
|
|
|
# --- CLI parsing -------------------------------------------------------------
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--token=*) TOKEN="${arg#*=}" ;;
|
|
--user=*) USERNAME="${arg#*=}" ;;
|
|
--registry=*) REGISTRY="${arg#*=}" ;;
|
|
--keep-tag=*) KEEP_TAG="${arg#*=}" ;;
|
|
--dry-run) DRY_RUN=1 ;;
|
|
--no-dry-run) DRY_RUN=0 ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*)
|
|
echo "ERROR: Unknown argument: $arg"
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$TOKEN" ]]; then
|
|
echo "ERROR: --token= is required"
|
|
usage
|
|
exit 1
|
|
fi
|
|
|
|
ACCEPT_MANIFEST="application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.v2+json"
|
|
|
|
get_bearer_token() {
|
|
local repo="$1"
|
|
local scope="repository:${repo}:pull,delete"
|
|
curl -fsS -u "${USERNAME}:${TOKEN}" \
|
|
"${REGISTRY}/v2/token?service=container_registry&scope=${scope}" \
|
|
| jq -r '.token // .access_token // empty'
|
|
}
|
|
|
|
auth_header_for_repo() {
|
|
local repo="$1"
|
|
local t
|
|
t="$(get_bearer_token "$repo")"
|
|
if [[ -z "$t" ]]; then
|
|
echo "ERROR: Could not obtain bearer token for ${repo} (check PAT permissions)."
|
|
exit 1
|
|
fi
|
|
echo "Authorization: Bearer ${t}"
|
|
}
|
|
|
|
tags_list() {
|
|
local repo="$1" auth="$2"
|
|
curl -fsS -H "$auth" "${REGISTRY}/v2/${repo}/tags/list"
|
|
}
|
|
|
|
digest_for_tag() {
|
|
local repo="$1" tag="$2" auth="$3"
|
|
curl -fsSI \
|
|
-H "$auth" \
|
|
-H "Accept: ${ACCEPT_MANIFEST}" \
|
|
"${REGISTRY}/v2/${repo}/manifests/${tag}" \
|
|
| tr -d '\r' \
|
|
| awk 'BEGIN{IGNORECASE=1} /^docker-content-digest:/ {print $2; exit}'
|
|
}
|
|
|
|
delete_digest() {
|
|
local repo="$1" digest="$2" auth="$3"
|
|
if [[ "$DRY_RUN" == "1" ]]; then
|
|
echo "DRY_RUN: DELETE ${REGISTRY}/v2/${repo}/manifests/${digest}"
|
|
return 0
|
|
fi
|
|
|
|
curl -fsS -X DELETE -H "$auth" \
|
|
"${REGISTRY}/v2/${repo}/manifests/${digest}" >/dev/null
|
|
}
|
|
|
|
purge_repo_keep_tag() {
|
|
local repo="$1"
|
|
echo "== Purging ${repo} (keep ${KEEP_TAG}) =="
|
|
|
|
local auth
|
|
auth="$(auth_header_for_repo "$repo")"
|
|
|
|
local json tags
|
|
json="$(tags_list "$repo" "$auth")"
|
|
tags="$(echo "$json" | jq -r '.tags[]?' | sort || true)"
|
|
|
|
if [[ -z "$tags" ]]; then
|
|
echo "No tags found."
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
if ! echo "$tags" | grep -qx "${KEEP_TAG}"; then
|
|
echo "WARN: No '${KEEP_TAG}' tag found; refusing to purge ${repo}."
|
|
echo "$tags" | sed 's/^/ - /'
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
local keep_digest
|
|
keep_digest="$(digest_for_tag "$repo" "$KEEP_TAG" "$auth" || true)"
|
|
if [[ -z "$keep_digest" ]]; then
|
|
echo "ERROR: Could not resolve digest for ${repo}:${KEEP_TAG}"
|
|
echo "Tip: curl -vI -H \"$auth\" ${REGISTRY}/v2/${repo}/manifests/${KEEP_TAG}"
|
|
echo
|
|
return 1
|
|
fi
|
|
echo "${KEEP_TAG} digest: ${keep_digest}"
|
|
|
|
# Collect digests referenced by non-KEEP_TAG tags (skipping those equal to keep_digest)
|
|
local digests=()
|
|
while read -r tag; do
|
|
[[ -z "$tag" || "$tag" == "$KEEP_TAG" ]] && continue
|
|
|
|
local d
|
|
d="$(digest_for_tag "$repo" "$tag" "$auth" || true)"
|
|
if [[ -z "$d" ]]; then
|
|
echo "WARN: couldn't resolve digest for tag=${tag} (skipping)"
|
|
continue
|
|
fi
|
|
|
|
if [[ "$d" == "$keep_digest" ]]; then
|
|
echo "SKIP: tag=${tag} points to ${KEEP_TAG} digest (deleting would remove ${KEEP_TAG} too)"
|
|
continue
|
|
fi
|
|
|
|
digests+=("$d")
|
|
done <<< "$tags"
|
|
|
|
if [[ "${#digests[@]}" -eq 0 ]]; then
|
|
echo "Nothing to delete."
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
echo "Will delete (deduped) manifest(s) (and all tags pointing to them)."
|
|
printf '%s\n' "${digests[@]}" | sort -u | while read -r digest; do
|
|
[[ -z "$digest" ]] && continue
|
|
echo "Deleting digest: $digest"
|
|
if ! delete_digest "$repo" "$digest" "$auth"; then
|
|
echo "WARN: delete failed for $digest"
|
|
fi
|
|
done
|
|
|
|
echo "Done."
|
|
echo
|
|
}
|
|
|
|
for repo in "${REPOS[@]}"; do
|
|
purge_repo_keep_tag "$repo"
|
|
done
|
|
|
|
echo "Complete."
|