-
-
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; |
Hello,
Can you please explain line 10 " jq .repositories | jq -r "
./nexuscleanup.sh: line 12: jq: command not found
./nexuscleanup.sh: line 12: jq: command not found
(23) Failed writing body
I got above error
Hello,
Can you please explain line 10 " jq .repositories | jq -r "
./nexuscleanup.sh: line 12: jq: command not found
./nexuscleanup.sh: line 12: jq: command not found
(23) Failed writing bodyI got above error
Install jq on the machine
Hello, Thanks a lot for this gist, it was exactly what I was looking for !
Anyway I'm facing an issue with the storage on disk because the image is deleted in nexus (not visible in the UI) but the disk space does not decrease Is there a way to really remove the image and layers from disk ? Am I missing a parameter in my Nexus configuration to really remove the images ?
My nexus version is 3.10 and even after running the "Docker - Delete unused manifests and images" task the disk space remains the same.
Thanks for your help.
You may need to consider running a "Compact blob store" task -- https://help.sonatype.com/repomanager3/nexus-repository-administration/repository-management/cleanup-policies?_ga=2.95007263.285540370.1704304621-1985157648.1703700486
How can I add a filter to remove images that are older than two weeks?
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}")
Hello,
Thanks a lot for this gist, it was exactly what I was looking for !
Anyway I'm facing an issue with the storage on disk because the image is deleted in nexus (not visible in the UI) but the disk space does not decrease
Is there a way to really remove the image and layers from disk ? Am I missing a parameter in my Nexus configuration to really remove the images ?
My nexus version is 3.10 and even after running the "Docker - Delete unused manifests and images" task the disk space remains the same.
Thanks for your help.