Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI cleanup #32

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 89 additions & 55 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,41 @@
import re

import boto3

try:
import requests
except ImportError:
from botocore.vendored import requests

REGION = None
DRYRUN = None
REGION = "ALL"
DRY_RUN = True
IMAGES_TO_KEEP = None
IGNORE_TAGS_REGEX = None


def initialize():
global REGION
global DRYRUN
global DRY_RUN
global IMAGES_TO_KEEP
global IGNORE_TAGS_REGEX

REGION = os.environ.get('REGION', "None")
DRYRUN = os.environ.get('DRYRUN', "false").lower()
if DRYRUN == "false":
DRYRUN = False
REGION = os.environ.get("REGION", "ALL")

if os.environ.get("DRY_RUN", "").lower() == "false":
DRY_RUN = False
else:
DRYRUN = True
IMAGES_TO_KEEP = int(os.environ.get('IMAGES_TO_KEEP', 100))
IGNORE_TAGS_REGEX = os.environ.get('IGNORE_TAGS_REGEX', "^$")
DRY_RUN = True

IMAGES_TO_KEEP = int(os.environ.get("IMAGES_TO_KEEP", 100))
IGNORE_TAGS_REGEX = os.environ.get("IGNORE_TAGS_REGEX", "^$")


def handler(event, context):
initialize()
if REGION == "None":
partitions = requests.get("https://raw.githubusercontent.com/boto/botocore/develop/botocore/data/endpoints.json").json()[
'partitions']
partitions = requests.get(
"https://raw.githubusercontent.com/boto/botocore/develop/botocore/data/endpoints.json"
).json()["partitions"]
if REGION == "ALL":
for partition in partitions:
if partition['partition'] == "aws":
for endpoint in partition['services']['ecs']['endpoints']:
Expand Down Expand Up @@ -98,7 +102,7 @@ def discover_delete_images(regionname):

for repository in repositories:
print("------------------------")
print("Starting with repository :" + repository['repositoryUri'])
print("Starting with repository:", repository["repositoryUri"])
deletesha = []
deletetag = []
tagged_images = []
Expand All @@ -114,7 +118,7 @@ def discover_delete_images(regionname):
append_to_list(deletesha, image['imageDigest'])

print("Total number of images found: {}".format(len(tagged_images) + len(deletesha)))
print("Number of untagged images found {}".format(len(deletesha)))
print("Number of untagged images found: {}".format(len(deletesha)))

tagged_images.sort(key=lambda k: k['imagePushedAt'], reverse=True)

Expand All @@ -128,7 +132,7 @@ def discover_delete_images(regionname):
if imageurl not in running_sha:
running_sha.append(image['imageDigest'])

print("Number of running images found {}".format(len(running_sha)))
print("Number of running images found: {}".format(len(running_sha)))

for image in tagged_images:
if tagged_images.index(image) >= IMAGES_TO_KEEP:
Expand All @@ -148,16 +152,16 @@ def discover_delete_images(regionname):
repository['repositoryName']
)
else:
print("Nothing to delete in repository : " + repository['repositoryName'])
print("Nothing to delete in repository: " + repository["repositoryName"])


def append_to_list(list, id):
if not {'imageDigest': id} in list:
list.append({'imageDigest': id})
if {"imageDigest": id} not in list:
list.append({"imageDigest": id})


def append_to_tag_list(list, id):
if not id in list:
if id not in list:
list.append(id)


Expand All @@ -167,49 +171,79 @@ def chunks(l, n):
yield l[i:i + n]


def delete_images(ecr_client, deletesha, deletetag, id, name):
if len(deletesha) >= 1:
## spliting list of images to delete on chunks with 100 images each
## http://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_BatchDeleteImage.html#API_BatchDeleteImage_RequestSyntax
i = 0
for deletesha_chunk in chunks(deletesha, 100):
i += 1
if not DRYRUN:
def delete_images(
ecr_client, image_ids_to_delete, tagged_for_deletion, repository_id, repository_name
):
if image_ids_to_delete:
# spliting list of images to delete on chunks with 100 images each
# http://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_BatchDeleteImage.html#API_BatchDeleteImage_RequestSyntax
for chunk_number, image_id_chunk in enumerate(
chunks(image_ids_to_delete, 100), start=1
):
if not DRY_RUN:
delete_response = ecr_client.batch_delete_image(
registryId=id,
repositoryName=name,
imageIds=deletesha_chunk
registryId=repository_id,
repositoryName=repository_name,
imageIds=image_id_chunk,
)
print(delete_response)
else:
print("registryId:" + id)
print("repositoryName:" + name)
print("Deleting {} chank of images".format(i))
print("imageIds:", end='')
print(deletesha_chunk)
if deletetag:
print(f"Repository: {repository_name} ({repository_id})")
print(f"Deleting chunk #{chunk_number}: {len(image_id_chunk)} images")
print("Image digests:")
for image_id in image_id_chunk:
print("\t", image_id["imageDigest"])

if tagged_for_deletion:
print("Image URLs that are marked for deletion:")
for ids in deletetag:
for ids in tagged_for_deletion:
print("- {} - {}".format(ids["imageUrl"], ids["pushedAt"]))


# Below is the test harness
if __name__ == '__main__':
request = {"None": "None"}
parser = argparse.ArgumentParser(description='Deletes stale ECR images')
parser.add_argument('-dryrun', help='Prints the repository to be deleted without deleting them', default='true',
action='store', dest='dryrun')
parser.add_argument('-imagestokeep', help='Number of image tags to keep', default='100', action='store',
dest='imagestokeep')
parser.add_argument('-region', help='ECR/ECS region', default=None, action='store', dest='region')
parser.add_argument('-ignoretagsregex', help='Regex of tag names to ignore', default="^$", action='store', dest='ignoretagsregex')
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Deletes stale ECR images",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)

# We want the user to explicitly opt-in to deleting images so we'll have a
# mutually-exclusive option group which requires running with either
# --dry-run or --delete-images
delete_mode = parser.add_mutually_exclusive_group(required=True)

delete_mode.add_argument(
"--dry-run",
help="Prints the images to be deleted without deleting them",
action="store_true",
dest="dry_run",
)
delete_mode.add_argument(
"--delete-images",
help="Delete the images (cancels --dry-run)",
action="store_false",
dest="dry_run",
)

parser.add_argument(
"--images-to-keep", help="Number of image tags to keep", default=100
)

parser.add_argument(
"--region",
help="AWS region",
default=os.environ.get("AWS_DEFAULT_REGION", "ALL"),
)

parser.add_argument(
"--ignore-tags-regex", help="Regex of tag names to ignore", default="^$"
)

args = parser.parse_args()
if args.region:
os.environ["REGION"] = args.region
else:
os.environ["REGION"] = "None"
os.environ["DRYRUN"] = args.dryrun.lower()
os.environ["IMAGES_TO_KEEP"] = args.imagestokeep
os.environ["IGNORE_TAGS_REGEX"] = args.ignoretagsregex
handler(request, None)

os.environ["REGION"] = args.region
os.environ["DRY_RUN"] = str(args.dry_run).lower()
os.environ["IMAGES_TO_KEEP"] = str(args.images_to_keep)
os.environ["IGNORE_TAGS_REGEX"] = args.ignore_tags_regex

handler({"None": "None"}, None)