-
-
Save matzegebbe/a2678b227add6bafad9a3a802618b5ad to your computer and use it in GitHub Desktop.
#!/bin/bash | |
REPO_URL="https://repository.xxx.net/repository/" | |
USER="admin" | |
PASSWORD="datpassword" | |
BUCKET="portal-docker" | |
KEEP_IMAGES=10 | |
IMAGES=$(curl --silent -X GET -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -u ${USER}:${PASSWORD} "${REPO_URL}${BUCKET}/v2/_catalog" | jq .repositories | jq -r '.[]' ) | |
echo ${IMAGES} | |
for IMAGE_NAME in ${IMAGES}; do | |
echo ${IMAGE_NAME} | |
TAGS=$(curl --silent -X GET -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -u ${USER}:${PASSWORD} "${REPO_URL}${BUCKET}/v2/${IMAGE_NAME}/tags/list" | jq .tags | jq -r '.[]' ) | |
TAG_COUNT=$(echo $TAGS | wc -w) | |
let TAG_COUNT_DEL=${TAG_COUNT}-${KEEP_IMAGES} | |
COUNTER=0 | |
echo "THERE ARE ${TAG_COUNT} IMAGES FOR ${IMAGE_NAME}" | |
## skip if smaller than keep | |
if [ "${KEEP_IMAGES}" -gt "${TAG_COUNT}" ] | |
then | |
echo "There are only ${TAG_COUNT} Images for ${IMAGE_NAME} - nothing to delete" | |
continue | |
fi | |
for TAG in ${TAGS}; do | |
let COUNTER=COUNTER+1 | |
if [ "${COUNTER}" -gt "${TAG_COUNT_DEL}" ] | |
then | |
break; | |
fi | |
IMAGE_SHA=$(curl --silent -I -X GET -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -u ${USER}:${PASSWORD} "${REPO_URL}${BUCKET}/v2/${IMAGE_NAME}/manifests/$TAG" | grep Docker-Content-Digest | cut -d ":" -f3 | tr -d '\r') | |
echo "DELETE ${TAG} ${IMAGE_SHA}"; | |
DEL_URL="${REPO_URL}${BUCKET}/v2/${IMAGE_NAME}/manifests/sha256:${IMAGE_SHA}" | |
RET="$(curl --silent -k -X DELETE -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -u ${USER}:${PASSWORD} $DEL_URL)" | |
done; | |
done; |
Be aware that the ordering of tags might not what you think! So probably 10 old builds are kept and new ones are deleted!
e.g.
https://your-repo.com/repository/docker-repo/v2/image-name/tags/list
{
"name": "image-name",
"tags": [
"0.1.12",
"0.1.14",
"0.1.16",
"0.1.18",
"0.1.20",
"0.1.22",
"0.1.4",
"0.1.5",
"0.1.689",
"0.1.690",
"0.1.691",
"0.1.692",
"0.1.693",
"0.1.7",
"0.1.8",
"0.1.9",
"latest"
]
}
I think this shouldn't be based on tags .. but created datetime. We use commit hashes as versions for example
First of all, thanks for the original script!
I have rewritten it so that the script uses the last-modified date to order the images.
It also skips the latest or master tags
for IMAGE_NAME in ${IMAGES}; do
# echo -e "\n${IMAGE_NAME}"
# get tags in repo
TAGS=$(curl --silent -X GET -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -u ${USER}:${PASSWORD} "${REPO_URL}${BUCKET}/v2/${IMAGE_NAME}/tags/list" | jq .tags | jq -r '.[]' )
TAG_COUNT=$(echo $TAGS | wc -w)
let TAG_COUNT_DEL=${TAG_COUNT}-${KEEP_IMAGES}
# echo "THERE ARE ${TAG_COUNT} IMAGES FOR ${IMAGE_NAME}"
# create empty array for dates
unset TAGS_WITH_DATES
declare -A TAGS_WITH_DATES
# put dates and tags in array
for TAG in $TAGS; do
IMAGE_SHA=$(curl --silent -I -X GET -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -u ${USER}:${PASSWORD} "${REPO_URL}${BUCKET}/v2/${IMAGE_NAME}/manifests/$TAG" | grep -i "Last-Modified" | sed 's/Last-Modified: //')
TIMESTAMP=$(date -d "$IMAGE_SHA" +%s)
TAGS_WITH_DATES["$TAG"]=$TIMESTAMP
done
## skip if smaller than keep
if [ "${KEEP_IMAGES}" -gt "${TAG_COUNT}" ]
then
# echo "There are only ${TAG_COUNT} Images for ${IMAGE_NAME} - nothing to delete"
continue
fi
# del tags
for TAG in $(for TAG in "${!TAGS_WITH_DATES[@]}"; do echo "$TAG ${TAGS_WITH_DATES[$TAG]}"; done | sort -k2 -n | head -n $TAG_COUNT_DEL | awk '{print $1}'); do
if [ "${TAG}" == "latest" ] || [ "${TAG}" == "master" ]
then
# echo "Skip latest/master tag"
continue
fi
# echo "$TAG"
# get TAG's SHA
IMAGE_SHA=$(curl --silent -I -X GET -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -u ${USER}:${PASSWORD} "${REPO_URL}${BUCKET}/v2/${IMAGE_NAME}/manifests/$TAG" | grep Docker-Content-Digest | cut -d ":" -f3 | tr -d '\r')
# del
READABLE_DATE=$(date -d @${TAGS_WITH_DATES[$TAG]} "+%Y-%m-%d %H:%M:%S")
echo "${IMAGE_NAME} - DEL $TAG with ${READABLE_DATE} time"
DEL_URL="${REPO_URL}${BUCKET}/v2/${IMAGE_NAME}/manifests/sha256:${IMAGE_SHA}"
# TEST FIRST!!! RET="$(curl --silent -k -X DELETE -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -u ${USER}:${PASSWORD} $DEL_URL)"
done;
Thanks for the script! im using it as a reference. Currently, i have implemented this script in my jenkins job to automatically delete a specific image tag after the job runs.
#!/bin/bash
REPO_URL="http://1.2.3.4:8081/repository/"
USERNAME="user"
PASSWORD="password"
BUCKET="dockerbucket"
KEEP_IMAGES=3
# IMAGE_DIR="nginx"
# IMAGE_NAME="nginx"
IMAGE_DIR="$1"
IMAGE_NAME="$2"
IMAGE_FULL_NAME=$IMAGE_DIR/$IMAGE_NAME
# echo "${IMAGE_FULL_NAME}"
TAGS=$(curl --silent -X GET -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -u ${USERNAME}:${PASSWORD} "${REPO_URL}${BUCKET}/v2/${IMAGE_FULL_NAME}/tags/list" | jq .tags | jq -r '.[]' | sort -r)
echo "$TAGS" | awk -v img="$IMAGE_NAME" '{print img ":" $0}'
TOTAL_TAGS=$(echo "$TAGS" | wc -l)
echo "total tags = $TOTAL_TAGS"
if [[ $TOTAL_TAGS -gt $KEEP_IMAGES ]]; then
echo "Total tags ($TOTAL_TAGS) exceed KEEP_IMAGES ($KEEP_IMAGES). Deleting older tags."
TAGS_TO_DELETE=$(echo "$TAGS" | tail -n +$((KEEP_IMAGES + 1)))
while IFS= read -r TAG; do
echo "Deleting image ${IMAGE_NAME}:$TAG"
echo "Executing curl request..."
IMAGE_SHA=$(curl --silent -I -X GET -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -u ${USERNAME}:${PASSWORD} "${REPO_URL}${BUCKET}/v2/${IMAGE_FULL_NAME}/manifests/$TAG" | grep Docker-Content-Digest | cut -d ":" -f3 | tr -d '\r')
echo "DELETE ${TAG} ${IMAGE_SHA}";
DEL_URL="${REPO_URL}${BUCKET}/v2/${IMAGE_FULL_NAME}/manifests/sha256:${IMAGE_SHA}"
RET="$(curl --silent -k -X DELETE -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -u ${USERNAME}:${PASSWORD} $DEL_URL)"
# curl -s -X DELETE -u ${USERNAME}:${PASSWORD} "${REPO_URL}${BUCKET}/v2/${IMAGE_FULL_NAME}/manifests/$TAG"
if [[ $? -ne 0 ]]; then
echo "Failed to delete image tag: ${IMAGE_NAME}:$TAG"
else
echo "Successfully deleted image tag: ${IMAGE_NAME}:$TAG"
fi
echo "----------------------------------------"
done <<< "$TAGS_TO_DELETE"
else
echo "Total tags ($TOTAL_TAGS) are within or equal to KEEP_IMAGES ($KEEP_IMAGES). No deletion needed."
fi
I use this keeps last 5 images based on crated date desc, works great. I have no idea why they don't add it just to a job .. ugh.
import requests
import argparse
import json
from datetime import datetime, timezone
# Function to print usage message
def usage():
print("Usage: nexus_docker_cleanup.py -u USER -p PASSWORD [-p PREFIX] [-k KEEP_IMAGES]")
exit(1)
# Default values
KEEP_IMAGES = 5
PREFIX = "my/services/"
# Argument parser
parser = argparse.ArgumentParser(description='Clean up Docker images from Nexus repository.')
parser.add_argument('-u', '--user', required=True, help='User for repository authentication')
parser.add_argument('-p', '--password', required=True, help='Password for repository authentication')
parser.add_argument('--prefix', default=PREFIX, help='Filter images by prefix')
parser.add_argument('-k', '--keep_images', default=KEEP_IMAGES, type=int, help='Number of images to keep')
args = parser.parse_args()
USER = args.user
PASSWORD = args.password
KEEP_IMAGES = args.keep_images
PREFIX = args.prefix
REPO_URL = "https://nexus.myorg/repository/"
BUCKET = "docker-mcm-hosted"
def get_json_response(url, headers):
response = requests.get(url, headers=headers, auth=(USER, PASSWORD))
if response.status_code != 200:
print(f"Error fetching URL {url}. Status code: {response.status_code}")
return None
return response.json()
def parse_isoformat(date_str):
try:
if 'Z' in date_str:
date_str = date_str.replace('Z', '+00:00')
if '+' in date_str or '-' in date_str:
# Workaround to strip the isoformat to maximum microsecond precision (6 digits)
base, frac_sec = date_str.split('.')
frac_sec = frac_sec[:6] # limit to 6 digits
if '+' in date_str:
offset = date_str.split('+')[1]
date_str = f"{base}.{frac_sec}+{offset}"
elif '-' in date_str:
offset = date_str.split('-')[1]
date_str = f"{base}.{frac_sec}-{offset}"
else:
date_str = date_str[:26] + "+00:00"
dt = datetime.fromisoformat(date_str)
# Ensure the datetime is timezone-aware
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
except ValueError:
print(f"Invalid isoformat string: '{date_str}'")
return None
# Fetch list of images
images_response = get_json_response(f"{REPO_URL}{BUCKET}/v2/_catalog",
{'Accept': 'application/vnd.docker.distribution.manifest.v2+json'})
if images_response is None:
exit(1)
IMAGES = images_response.get('repositories', [])
# Filter images based on the prefix
filtered_images = [image for image in IMAGES if image.startswith(PREFIX)]
print(f"\nFiltered list of images with prefix '{PREFIX}':")
print(filtered_images)
for IMAGE_NAME in filtered_images:
print(IMAGE_NAME)
tags_response = get_json_response(f"{REPO_URL}{BUCKET}/v2/{IMAGE_NAME}/tags/list",
{'Accept': 'application/vnd.docker.distribution.manifest.v2+json'})
if tags_response is None:
continue
TAGS = tags_response.get('tags', [])
image_tags_with_date = []
for TAG in TAGS:
manifest_response = get_json_response(f"{REPO_URL}{BUCKET}/v2/{IMAGE_NAME}/manifests/{TAG}",
{'Accept': 'application/vnd.docker.distribution.manifest.v2+json'})
if manifest_response is None:
continue
config_digest = manifest_response.get('config', {}).get('digest')
if config_digest:
blob_response = get_json_response(f"{REPO_URL}{BUCKET}/v2/{IMAGE_NAME}/blobs/{config_digest}",
{'Accept': 'application/vnd.docker.distribution.manifest.v2+json'})
if blob_response is None:
continue
creation_date = blob_response.get('created')
if creation_date:
parsed_date = parse_isoformat(creation_date)
if parsed_date:
image_tags_with_date.append((TAG, parsed_date))
# Sort tags by creation date descending
image_tags_with_date.sort(key=lambda x: x[1], reverse=True)
TAGS_TO_KEEP = image_tags_with_date[:KEEP_IMAGES]
TAGS_TO_DELETE = image_tags_with_date[KEEP_IMAGES:]
print(f"THERE ARE {len(TAGS)} IMAGES FOR {IMAGE_NAME}")
for tag, creation_date in TAGS_TO_DELETE:
delete_response = requests.head(f"{REPO_URL}{BUCKET}/v2/{IMAGE_NAME}/manifests/{tag}",
headers={'Accept': 'application/vnd.docker.distribution.manifest.v2+json'},
auth=(USER, PASSWORD))
if delete_response.status_code != 200:
print(f"Error fetching manifest for {IMAGE_NAME}:{tag}. Status code: {delete_response.status_code}")
continue
image_sha = delete_response.headers.get('Docker-Content-Digest').split(':')[-1].strip()
print(f"DELETE {tag} {image_sha} created at {creation_date}")
del_url = f"{REPO_URL}{BUCKET}/v2/{IMAGE_NAME}/manifests/sha256:{image_sha}"
del_response = requests.delete(del_url,
headers={'Accept': 'application/vnd.docker.distribution.manifest.v2+json'},
auth=(USER, PASSWORD))
if del_response.status_code != 202:
print(f"Error deleting {IMAGE_NAME}:{tag} with SHA {image_sha}")
else:
print(f"Successfully deleted {IMAGE_NAME}:{tag} with SHA {image_sha}, created at {creation_date}")
How can I add a filter to remove images that are older than two weeks?