-
Notifications
You must be signed in to change notification settings - Fork 4
docker-rotate 2.0 #8
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,3 +55,4 @@ target/ | |
|
||
cover | ||
.eggs | ||
.venv* |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,24 @@ | ||
docker-rotate | ||
============= | ||
|
||
In a continuously deployed environment, old and unused docker images accumulate and use up space. | ||
`docker-rotate` helps remove the K oldest images of each type. | ||
In a continuously deployed environment, old and unused docker images and containers accumulate and use up space. | ||
`docker-rotate` helps remove the K oldest images of each type and remove non-running containers. | ||
|
||
[](https://travis-ci.org/locationlabs/docker-rotate) | ||
|
||
Usage: | ||
|
||
# delete all but the three oldest images of each type | ||
docker-rotate --clean-images --keep 3 | ||
# delete all but the three most recent images of each type | ||
docker-rotate images --keep 3 | ||
|
||
# only target one type of image (by name) | ||
docker-rotate --clean-images --keep 3 --only organization/image | ||
# only target one type of image but don't remove latest | ||
docker-rotate images --keep 3 --image "organization/image" "~:latest" | ||
|
||
# don't actualy delete anything | ||
docker-rotate --clean-images --keep 3 --dry-run | ||
docker-rotate --dry-run images --keep 3 | ||
|
||
# also delete exited containers (except those with volumes) | ||
docker-rotate --clean-images --clean-containers --keep 3 | ||
# delete containers exited more than an hour ago | ||
docker-rotate containers --exited 1h | ||
|
||
By default, `docker-rotate` connects to the local Unix socket; the usual environment variables will | ||
be respected if the `--use-env` flag is given. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
from datetime import datetime, timedelta | ||
from dateutil import parser | ||
from dateutil.tz import tzutc | ||
import re | ||
|
||
from docker.errors import APIError | ||
|
||
from dockerrotate.filter import include_image | ||
|
||
|
||
TIME_REGEX = re.compile(r'((?P<days>\d+?)d)?((?P<hours>\d+?)h)?((?P<minutes>\d+?)m)?((?P<seconds>\d+?)s)?') # noqa | ||
|
||
|
||
def parse_time(time_str): | ||
""" | ||
Parse a human readable time delta string. | ||
""" | ||
parts = TIME_REGEX.match(time_str) | ||
if not parts: | ||
raise Exception("Invalid time delta format '{}'".format(time_str)) | ||
parts = parts.groupdict() | ||
time_params = {} | ||
for (name, param) in parts.iteritems(): | ||
if param: | ||
time_params[name] = int(param) | ||
return timedelta(**time_params) | ||
|
||
|
||
def include_container(container, args): | ||
""" | ||
Return truthy if container should be removed. | ||
""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only exited containers or containers that have been created but not started are considered for removal. Note that I removed the support for data containers. |
||
inspect_data = args.client.inspect_container(container["Id"]) | ||
status = inspect_data["State"]["Status"] | ||
|
||
if status == "exited": | ||
finished_at = parser.parse(inspect_data["State"]["FinishedAt"]) | ||
if (args.now - finished_at) < args.exited_ts: | ||
return False | ||
elif status == "created": | ||
created_at = parser.parse(inspect_data["Created"]) | ||
if (args.now - created_at) < args.created_ts: | ||
return False | ||
else: | ||
return False | ||
|
||
return include_image([container["Image"]], args) | ||
|
||
|
||
def clean_containers(args): | ||
""" | ||
Delete non-running containers. | ||
Images cannot be deleted if in use. Deleting dead containers allows | ||
more images to be cleaned. | ||
""" | ||
args.exited_ts = parse_time(args.exited) | ||
args.created_ts = parse_time(args.created) | ||
args.now = datetime.now(tzutc()) | ||
|
||
containers = [ | ||
container for container in args.client.containers(all=True) | ||
if include_container(container, args) | ||
] | ||
|
||
for container in containers: | ||
print "Removing container ID: {}, Name: {}, Image: {}".format( | ||
container["Id"], | ||
(container.get("Names") or ["N/A"])[0], | ||
container["Image"], | ||
) | ||
|
||
if args.dry_run: | ||
continue | ||
|
||
try: | ||
args.client.remove_container(container["Id"]) | ||
except APIError as error: | ||
print "Unable to remove container: {}: {}".format( | ||
container["Id"], | ||
error, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import re | ||
|
||
|
||
def include_image(image_tags, args): | ||
""" | ||
Return truthy if image should be considered for removal. | ||
""" | ||
if not args.images: | ||
return True | ||
|
||
return all(regex_match(pattern, tag) | ||
for pattern in args.images | ||
for tag in image_tags) | ||
|
||
|
||
def regex_match(pattern, tag): | ||
""" | ||
Perform a regex match on the tag. | ||
""" | ||
if pattern[0] == '~': | ||
return not re.search(pattern[1:], tag) | ||
return re.search(pattern, tag) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
from collections import defaultdict | ||
|
||
from docker.errors import APIError | ||
|
||
from dockerrotate.filter import include_image | ||
|
||
|
||
def clean_images(args): | ||
""" | ||
Delete old images keeping the most recent N images by tag. | ||
""" | ||
# should not need to inspect all images; only intermediate images should appear | ||
# when all is true; these should be deleted along with dependent images | ||
images = [image | ||
for image in args.client.images(all=False) | ||
if include_image(image["RepoTags"], args)] | ||
|
||
# index by id | ||
images_by_id = { | ||
image["Id"]: image for image in images | ||
} | ||
|
||
# group by name | ||
images_by_name = defaultdict(set) | ||
for image in images: | ||
for tag in image["RepoTags"]: | ||
image_name = normalize_tag_name(tag) | ||
images_by_name[image_name].add(image["Id"]) | ||
|
||
for image_name, image_ids in images_by_name.items(): | ||
# sort/keep | ||
images_to_delete = sorted([ | ||
images_by_id[image_id] for image_id in image_ids], | ||
key=lambda image: -image["Created"], | ||
)[args.keep:] | ||
|
||
# delete | ||
for image in images_to_delete: | ||
print "Removing image ID: {}, Tags: {}".format( | ||
image["Id"], | ||
", ".join(image["RepoTags"]) | ||
) | ||
|
||
if args.dry_run: | ||
continue | ||
|
||
try: | ||
args.client.remove_image(image["Id"], force=True, noprune=False) | ||
except APIError as error: | ||
print error.message | ||
|
||
|
||
def normalize_tag_name(tag): | ||
""" | ||
docker-py provides image names with tags as a single string. | ||
|
||
We want: | ||
|
||
some.domain.com/organization/image:tag -> organization/image | ||
organization/image:tag -> organization/image | ||
image:tag -> image | ||
""" | ||
return "/".join(tag.rsplit(":", 1)[0].split("/")[-2:]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you removing containers? are you not naming them properly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what docker-rotate is doing - clearing out unused images and non-running containers to clear up space on the host