mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Cleanup docker publish (#42693)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #42691 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. n/a ## Testing - [ ] Added/updated automated tests - [X] QA'd all new/changed functionality manually - I ran the updated snapshot action on this branch and verified that it pushed the branch-tagged image, but not the SHA-tagged one. - I ran the cleanup script in dry-run mode and verified that it didn't expect to delete any non-sha-tagged images - I wasn't able to test the delete-image-on-branch-delete action for obvious reasons. - I haven't tested the cleanup script in non-dry-run mode... I could do on my personal dockerhub... <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Automated cleanup of Docker images when development branches are deleted to maintain registry hygiene. * New utility for managing and cleaning up legacy Docker image tags. * **Chores** * Enhanced Docker image tagging in snapshot builds with improved branch name handling. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
a9660f7e6a
commit
854fa2af62
5 changed files with 379 additions and 22 deletions
86
.github/workflows/docker-cleanup-branch.yaml
vendored
Normal file
86
.github/workflows/docker-cleanup-branch.yaml
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
name: Docker cleanup (branch deletion)
|
||||||
|
|
||||||
|
on:
|
||||||
|
delete:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cleanup:
|
||||||
|
# Only run for branch deletions (not tag deletions) in the fleetdm/fleet repo.
|
||||||
|
if: ${{ github.event.ref_type == 'branch' && github.repository == 'fleetdm/fleet' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: Docker Hub
|
||||||
|
steps:
|
||||||
|
- name: Sanitize branch name
|
||||||
|
id: sanitize
|
||||||
|
env:
|
||||||
|
BRANCH: ${{ github.event.ref }}
|
||||||
|
run: |
|
||||||
|
SANITIZED="${BRANCH//\//-}"
|
||||||
|
echo "TAG=$SANITIZED" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Skip protected branches
|
||||||
|
id: check_protected
|
||||||
|
env:
|
||||||
|
TAG: ${{ steps.sanitize.outputs.TAG }}
|
||||||
|
run: |
|
||||||
|
if [[ "$TAG" == "main" || "$TAG" == rc-minor-* || "$TAG" == rc-patch-* ]]; then
|
||||||
|
echo "skip=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Skipping cleanup for protected branch tag: $TAG"
|
||||||
|
else
|
||||||
|
echo "skip=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Delete tag from Docker Hub
|
||||||
|
if: steps.check_protected.outputs.skip == 'false'
|
||||||
|
env:
|
||||||
|
TAG: ${{ steps.sanitize.outputs.TAG }}
|
||||||
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
DOCKERHUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
|
||||||
|
run: |
|
||||||
|
# Authenticate and get JWT
|
||||||
|
TOKEN=$(curl -s -X POST "https://hub.docker.com/v2/users/login/" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"username\": \"$DOCKERHUB_USERNAME\", \"password\": \"$DOCKERHUB_ACCESS_TOKEN\"}" \
|
||||||
|
| jq -r .token)
|
||||||
|
|
||||||
|
# Bail if the token is empty (authentication failed)
|
||||||
|
if [[ -z "$TOKEN" ]]; then
|
||||||
|
echo "Failed to authenticate with Docker Hub. Check credentials."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete the tag (ignore 404 — tag may not exist)
|
||||||
|
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
|
"https://hub.docker.com/v2/repositories/fleetdm/fleet/tags/${TAG}/" \
|
||||||
|
-H "Authorization: Bearer $TOKEN")
|
||||||
|
|
||||||
|
if [[ "$HTTP_STATUS" == "204" ]]; then
|
||||||
|
echo "Deleted Docker Hub tag: $TAG"
|
||||||
|
elif [[ "$HTTP_STATUS" == "404" ]]; then
|
||||||
|
echo "Docker Hub tag not found (already deleted or never published): $TAG"
|
||||||
|
else
|
||||||
|
echo "Unexpected response from Docker Hub: HTTP $HTTP_STATUS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Delete tag from Quay.io
|
||||||
|
if: steps.check_protected.outputs.skip == 'false'
|
||||||
|
env:
|
||||||
|
TAG: ${{ steps.sanitize.outputs.TAG }}
|
||||||
|
QUAY_REGISTRY_PASSWORD: ${{ secrets.QUAY_REGISTRY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
|
"https://quay.io/api/v1/repository/fleetdm/fleet/tag/${TAG}" \
|
||||||
|
-H "Authorization: Bearer $QUAY_REGISTRY_PASSWORD")
|
||||||
|
|
||||||
|
if [[ "$HTTP_STATUS" == "204" || "$HTTP_STATUS" == "200" ]]; then
|
||||||
|
echo "Deleted Quay.io tag: $TAG"
|
||||||
|
elif [[ "$HTTP_STATUS" == "404" ]]; then
|
||||||
|
echo "Quay.io tag not found (already deleted or never published): $TAG"
|
||||||
|
else
|
||||||
|
echo "Unexpected response from Quay.io: HTTP $HTTP_STATUS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
38
.github/workflows/goreleaser-snapshot-fleet.yaml
vendored
38
.github/workflows/goreleaser-snapshot-fleet.yaml
vendored
|
|
@ -69,6 +69,14 @@ jobs:
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: make deps
|
run: make deps
|
||||||
|
|
||||||
|
- name: Sanitize branch name for Docker tag
|
||||||
|
id: sanitize_branch
|
||||||
|
env:
|
||||||
|
BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||||
|
run: |
|
||||||
|
SANITIZED="${BRANCH//\//-}"
|
||||||
|
echo "DOCKER_IMAGE_TAG=$SANITIZED" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Compute version from branch
|
- name: Compute version from branch
|
||||||
id: compute_version
|
id: compute_version
|
||||||
env:
|
env:
|
||||||
|
|
@ -90,14 +98,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||||
FLEET_VERSION: ${{ steps.compute_version.outputs.FLEET_VERSION }}
|
FLEET_VERSION: ${{ steps.compute_version.outputs.FLEET_VERSION }}
|
||||||
|
DOCKER_IMAGE_TAG: ${{ steps.sanitize_branch.outputs.DOCKER_IMAGE_TAG }}
|
||||||
- name: Tag image with branch name
|
|
||||||
run: docker tag fleetdm/fleet:$(git rev-parse --short HEAD) fleetdm/fleet:$(git rev-parse --abbrev-ref HEAD)
|
|
||||||
|
|
||||||
- name: Generate tag
|
|
||||||
id: generate_tag
|
|
||||||
run: |
|
|
||||||
echo "FLEET_IMAGE_TAG=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: List VEX files
|
- name: List VEX files
|
||||||
id: generate_vex_files
|
id: generate_vex_files
|
||||||
|
|
@ -125,7 +126,7 @@ jobs:
|
||||||
--pkg-types=os,library \
|
--pkg-types=os,library \
|
||||||
--severity=HIGH,CRITICAL \
|
--severity=HIGH,CRITICAL \
|
||||||
--vex="${{ steps.generate_vex_files.outputs.VEX_FILES }}" \
|
--vex="${{ steps.generate_vex_files.outputs.VEX_FILES }}" \
|
||||||
fleetdm/fleet:${{ steps.generate_tag.outputs.FLEET_IMAGE_TAG }}
|
fleetdm/fleet:${{ steps.sanitize_branch.outputs.DOCKER_IMAGE_TAG }}
|
||||||
|
|
||||||
- name: Check high/critical vulnerabilities before publishing (docker scout)
|
- name: Check high/critical vulnerabilities before publishing (docker scout)
|
||||||
# Only run this when tagging RCs.
|
# Only run this when tagging RCs.
|
||||||
|
|
@ -133,7 +134,7 @@ jobs:
|
||||||
uses: docker/scout-action@381b657c498a4d287752e7f2cfb2b41823f566d9 # v1.17.1
|
uses: docker/scout-action@381b657c498a4d287752e7f2cfb2b41823f566d9 # v1.17.1
|
||||||
with:
|
with:
|
||||||
command: cves
|
command: cves
|
||||||
image: fleetdm/fleet:${{ steps.generate_tag.outputs.FLEET_IMAGE_TAG }}
|
image: fleetdm/fleet:${{ steps.sanitize_branch.outputs.DOCKER_IMAGE_TAG }}
|
||||||
only-severities: critical,high
|
only-severities: critical,high
|
||||||
only-fixed: true
|
only-fixed: true
|
||||||
only-vex-affected: true
|
only-vex-affected: true
|
||||||
|
|
@ -145,14 +146,9 @@ jobs:
|
||||||
- name: Publish Docker images
|
- name: Publish Docker images
|
||||||
run: docker push fleetdm/fleet --all-tags
|
run: docker push fleetdm/fleet --all-tags
|
||||||
|
|
||||||
- name: Get tags
|
|
||||||
run: |
|
|
||||||
echo "TAG=$(git rev-parse --abbrev-ref HEAD) $(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
|
||||||
id: docker
|
|
||||||
|
|
||||||
- name: List tags for push
|
- name: List tags for push
|
||||||
run: |
|
run: |
|
||||||
echo "The following TAGs are to be pushed: ${{ steps.docker.outputs.TAG }}"
|
echo "The following tag will be pushed: ${{ steps.sanitize_branch.outputs.DOCKER_IMAGE_TAG }}"
|
||||||
|
|
||||||
- name: Login to quay.io
|
- name: Login to quay.io
|
||||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0
|
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0
|
||||||
|
|
@ -162,12 +158,12 @@ jobs:
|
||||||
password: ${{ secrets.QUAY_REGISTRY_PASSWORD }}
|
password: ${{ secrets.QUAY_REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
- name: Tag and push to quay.io
|
- name: Tag and push to quay.io
|
||||||
|
env:
|
||||||
|
TAG: ${{ steps.sanitize_branch.outputs.DOCKER_IMAGE_TAG }}
|
||||||
run: |
|
run: |
|
||||||
for TAG in ${{ steps.docker.outputs.TAG }}; do
|
docker tag fleetdm/fleet:${TAG} quay.io/fleetdm/fleet:${TAG}
|
||||||
docker tag fleetdm/fleet:${TAG} quay.io/fleetdm/fleet:${TAG}
|
for i in {1..5}; do
|
||||||
for i in {1..5}; do
|
docker push quay.io/fleetdm/fleet:${TAG} && break || sleep 10
|
||||||
docker push quay.io/fleetdm/fleet:${TAG} && break || sleep 10
|
|
||||||
done
|
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Slack notification
|
- name: Slack notification
|
||||||
|
|
|
||||||
|
|
@ -66,4 +66,4 @@ dockers:
|
||||||
- fleetctl
|
- fleetctl
|
||||||
dockerfile: tools/fleet-docker/Dockerfile
|
dockerfile: tools/fleet-docker/Dockerfile
|
||||||
image_templates:
|
image_templates:
|
||||||
- "fleetdm/fleet:{{ .ShortCommit }}"
|
- 'fleetdm/fleet:{{ envOrDefault "DOCKER_IMAGE_TAG" .Branch }}'
|
||||||
|
|
|
||||||
146
tools/cleanup-docker-sha-tags/cleanup-dockerhub-sha-tags.sh
Executable file
146
tools/cleanup-docker-sha-tags/cleanup-dockerhub-sha-tags.sh
Executable file
|
|
@ -0,0 +1,146 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# cleanup-dockerhub-sha-tags.sh — One-time script to remove commit-SHA-tagged
|
||||||
|
# Docker images from Docker Hub for the fleetdm/fleet repository.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./cleanup-dockerhub-sha-tags.sh [--dry-run] [--delay SECONDS]
|
||||||
|
#
|
||||||
|
# Environment variables:
|
||||||
|
# DOCKERHUB_USERNAME Docker Hub username (required)
|
||||||
|
# DOCKERHUB_ACCESS_TOKEN Docker Hub access token (required)
|
||||||
|
#
|
||||||
|
# This script is intended to be run manually by an engineer. It is NOT wired
|
||||||
|
# into any CI workflow. Requires jq and curl to be installed.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DRY_RUN=false
|
||||||
|
DELAY=1
|
||||||
|
REPO="fleetdm/fleet"
|
||||||
|
SHA_PATTERN="^[0-9a-f]{7,12}$"
|
||||||
|
PAGE_SIZE=100
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run) DRY_RUN=true; shift ;;
|
||||||
|
--delay) DELAY="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "${DOCKERHUB_USERNAME:-}" || -z "${DOCKERHUB_ACCESS_TOKEN:-}" ]]; then
|
||||||
|
echo "Error: DOCKERHUB_USERNAME and DOCKERHUB_ACCESS_TOKEN must be set." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Authenticate
|
||||||
|
echo "Authenticating to Docker Hub..."
|
||||||
|
DOCKERHUB_TOKEN=$(curl -s -X POST "https://hub.docker.com/v2/users/login/" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"username\": \"$DOCKERHUB_USERNAME\", \"password\": \"$DOCKERHUB_ACCESS_TOKEN\"}" \
|
||||||
|
| jq -r .token)
|
||||||
|
|
||||||
|
if [[ -z "$DOCKERHUB_TOKEN" || "$DOCKERHUB_TOKEN" == "null" ]]; then
|
||||||
|
echo "Error: Failed to authenticate to Docker Hub." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Authenticated."
|
||||||
|
|
||||||
|
# Collect all tags via pagination
|
||||||
|
echo "Fetching tags from Docker Hub (this may take a while)..."
|
||||||
|
ALL_TAGS=()
|
||||||
|
NEXT_URL="https://hub.docker.com/v2/repositories/${REPO}/tags/?page_size=${PAGE_SIZE}"
|
||||||
|
|
||||||
|
while [[ -n "$NEXT_URL" && "$NEXT_URL" != "null" ]]; do
|
||||||
|
RESPONSE=$(curl -s "$NEXT_URL" -H "Authorization: Bearer $DOCKERHUB_TOKEN")
|
||||||
|
PAGE_TAGS=$(echo "$RESPONSE" | jq -r '.results[].name')
|
||||||
|
while IFS= read -r tag; do
|
||||||
|
[[ -n "$tag" ]] && ALL_TAGS+=("$tag")
|
||||||
|
done <<< "$PAGE_TAGS"
|
||||||
|
NEXT_URL=$(echo "$RESPONSE" | jq -r '.next')
|
||||||
|
echo " Fetched ${#ALL_TAGS[@]} tags so far..."
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Total tags found: ${#ALL_TAGS[@]}"
|
||||||
|
|
||||||
|
# Filter to SHA-only tags
|
||||||
|
SHA_TAGS=()
|
||||||
|
for tag in "${ALL_TAGS[@]}"; do
|
||||||
|
if [[ "$tag" =~ $SHA_PATTERN ]]; then
|
||||||
|
SHA_TAGS+=("$tag")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Tags matching commit SHA pattern: ${#SHA_TAGS[@]}"
|
||||||
|
|
||||||
|
if [[ ${#SHA_TAGS[@]} -eq 0 ]]; then
|
||||||
|
echo "No SHA tags to delete. Done."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "=== Tags that will be KEPT (do not match SHA pattern) ==="
|
||||||
|
KEPT=0
|
||||||
|
for tag in "${ALL_TAGS[@]}"; do
|
||||||
|
if ! [[ "$tag" =~ $SHA_PATTERN ]]; then
|
||||||
|
echo " $tag"
|
||||||
|
KEPT=$((KEPT + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "=== ${KEPT} tags kept ==="
|
||||||
|
echo ""
|
||||||
|
echo "=== DRY RUN — ${#SHA_TAGS[@]} tags would be deleted ==="
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete tags
|
||||||
|
DELETED=0
|
||||||
|
FAILED=0
|
||||||
|
TOTAL=${#SHA_TAGS[@]}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Deleting ${TOTAL} SHA tags from Docker Hub..."
|
||||||
|
|
||||||
|
for tag in "${SHA_TAGS[@]}"; do
|
||||||
|
ATTEMPT=0
|
||||||
|
MAX_RETRIES=3
|
||||||
|
while true; do
|
||||||
|
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
|
"https://hub.docker.com/v2/repositories/${REPO}/tags/${tag}/" \
|
||||||
|
-H "Authorization: Bearer $DOCKERHUB_TOKEN")
|
||||||
|
|
||||||
|
if [[ "$HTTP_STATUS" == "204" ]]; then
|
||||||
|
DELETED=$((DELETED + 1))
|
||||||
|
break
|
||||||
|
elif [[ "$HTTP_STATUS" == "404" ]]; then
|
||||||
|
break
|
||||||
|
elif [[ "$HTTP_STATUS" == "429" ]]; then
|
||||||
|
ATTEMPT=$((ATTEMPT + 1))
|
||||||
|
if [[ $ATTEMPT -ge $MAX_RETRIES ]]; then
|
||||||
|
echo " FAILED (rate limited, max retries): $tag"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
BACKOFF=$((DELAY * ATTEMPT * 5))
|
||||||
|
echo " Rate limited, waiting ${BACKOFF}s before retry..."
|
||||||
|
sleep "$BACKOFF"
|
||||||
|
else
|
||||||
|
echo " FAILED (HTTP $HTTP_STATUS): $tag"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Progress
|
||||||
|
PROCESSED=$((DELETED + FAILED))
|
||||||
|
if (( PROCESSED % 50 == 0 )); then
|
||||||
|
echo " Progress: ${PROCESSED}/${TOTAL} processed (${DELETED} deleted, ${FAILED} failed)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$DELAY"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done. ${DELETED} deleted, ${FAILED} failed (out of ${TOTAL})"
|
||||||
129
tools/cleanup-docker-sha-tags/cleanup-quay-sha-tags.sh
Executable file
129
tools/cleanup-docker-sha-tags/cleanup-quay-sha-tags.sh
Executable file
|
|
@ -0,0 +1,129 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# cleanup-quay-sha-tags.sh — One-time script to remove commit-SHA-tagged
|
||||||
|
# Docker images from Quay.io for the fleetdm/fleet repository.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./cleanup-quay-sha-tags.sh [--dry-run] [--delay SECONDS]
|
||||||
|
#
|
||||||
|
# Environment variables:
|
||||||
|
# QUAY_REGISTRY_PASSWORD Quay.io bearer token (required)
|
||||||
|
#
|
||||||
|
# This script is intended to be run manually by an engineer. It is NOT wired
|
||||||
|
# into any CI workflow. Requires jq and curl to be installed.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DRY_RUN=false
|
||||||
|
DELAY=1
|
||||||
|
REPO="fleetdm/fleet"
|
||||||
|
SHA_PATTERN="^[0-9a-f]{7,12}$"
|
||||||
|
PAGE_SIZE=100
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run) DRY_RUN=true; shift ;;
|
||||||
|
--delay) DELAY="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "${QUAY_REGISTRY_PASSWORD:-}" ]]; then
|
||||||
|
echo "Error: QUAY_REGISTRY_PASSWORD must be set." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all tags via pagination
|
||||||
|
echo "Fetching tags from Quay.io (this may take a while)..."
|
||||||
|
ALL_TAGS=()
|
||||||
|
PAGE=1
|
||||||
|
HAS_MORE=true
|
||||||
|
|
||||||
|
while [[ "$HAS_MORE" == "true" ]]; do
|
||||||
|
RESPONSE=$(curl -s "https://quay.io/api/v1/repository/${REPO}/tag/?page=${PAGE}&limit=${PAGE_SIZE}" \
|
||||||
|
-H "Authorization: Bearer $QUAY_REGISTRY_PASSWORD")
|
||||||
|
|
||||||
|
PAGE_TAGS=$(echo "$RESPONSE" | jq -r '.tags[].name // empty')
|
||||||
|
COUNT=0
|
||||||
|
while IFS= read -r tag; do
|
||||||
|
if [[ -n "$tag" ]]; then
|
||||||
|
ALL_TAGS+=("$tag")
|
||||||
|
COUNT=$((COUNT + 1))
|
||||||
|
fi
|
||||||
|
done <<< "$PAGE_TAGS"
|
||||||
|
|
||||||
|
HAS_MORE=$(echo "$RESPONSE" | jq -r '.has_additional')
|
||||||
|
PAGE=$((PAGE + 1))
|
||||||
|
echo " Fetched ${#ALL_TAGS[@]} tags so far..."
|
||||||
|
|
||||||
|
if [[ $COUNT -eq 0 ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Total tags found: ${#ALL_TAGS[@]}"
|
||||||
|
|
||||||
|
# Filter to SHA-only tags
|
||||||
|
SHA_TAGS=()
|
||||||
|
for tag in "${ALL_TAGS[@]}"; do
|
||||||
|
if [[ "$tag" =~ $SHA_PATTERN ]]; then
|
||||||
|
SHA_TAGS+=("$tag")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Tags matching commit SHA pattern: ${#SHA_TAGS[@]}"
|
||||||
|
|
||||||
|
if [[ ${#SHA_TAGS[@]} -eq 0 ]]; then
|
||||||
|
echo "No SHA tags to delete. Done."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "=== Tags that will be KEPT (do not match SHA pattern) ==="
|
||||||
|
KEPT=0
|
||||||
|
for tag in "${ALL_TAGS[@]}"; do
|
||||||
|
if ! [[ "$tag" =~ $SHA_PATTERN ]]; then
|
||||||
|
echo " $tag"
|
||||||
|
KEPT=$((KEPT + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "=== ${KEPT} tags kept ==="
|
||||||
|
echo ""
|
||||||
|
echo "=== DRY RUN — ${#SHA_TAGS[@]} tags would be deleted ==="
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete tags
|
||||||
|
DELETED=0
|
||||||
|
FAILED=0
|
||||||
|
TOTAL=${#SHA_TAGS[@]}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Deleting ${TOTAL} SHA tags from Quay.io..."
|
||||||
|
|
||||||
|
for tag in "${SHA_TAGS[@]}"; do
|
||||||
|
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
|
"https://quay.io/api/v1/repository/${REPO}/tag/${tag}" \
|
||||||
|
-H "Authorization: Bearer $QUAY_REGISTRY_PASSWORD")
|
||||||
|
|
||||||
|
if [[ "$HTTP_STATUS" == "200" || "$HTTP_STATUS" == "204" ]]; then
|
||||||
|
DELETED=$((DELETED + 1))
|
||||||
|
elif [[ "$HTTP_STATUS" == "404" ]]; then
|
||||||
|
: # already gone, not a failure
|
||||||
|
else
|
||||||
|
echo " FAILED (HTTP $HTTP_STATUS): $tag"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Progress
|
||||||
|
PROCESSED=$((DELETED + FAILED))
|
||||||
|
if (( PROCESSED % 50 == 0 )); then
|
||||||
|
echo " Progress: ${PROCESSED}/${TOTAL} processed (${DELETED} deleted, ${FAILED} failed)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$DELAY"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done. ${DELETED} deleted, ${FAILED} failed (out of ${TOTAL})"
|
||||||
Loading…
Reference in a new issue