Skip to content

Commit

Permalink
Support excluding some images from removal.
Browse files Browse the repository at this point in the history
  • Loading branch information
Daniel Nephin committed Jul 20, 2015
1 parent bbf53ed commit b45fd97
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 21 deletions.
16 changes: 16 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ Example:
dcgc --max-container-age 3days --max-image-age 30days


Prevent images from being removed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``dcgc`` supports an image exclude list. If you have images that you'd like
to keep around forever you can use the exclude list to prevent them from
being removed.

--exclude-image
Never remove images with this tag. May be specified more than once.

--exclude-image-file
Path to a file which contains a list of images to exclude, one
image tag per line.



dcstop
------

Expand Down
2 changes: 1 addition & 1 deletion docker_custodian/__about__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# -*- coding: utf8 -*-

__version_info__ = (0, 4, 0)
__version_info__ = (0, 5, 0)
__version__ = '%d.%d.%d' % __version_info__
58 changes: 47 additions & 11 deletions docker_custodian/docker_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,28 @@ def get_all_images(client):
return images


def cleanup_images(client, max_image_age, dry_run):
def cleanup_images(client, max_image_age, dry_run, exclude_set):
# re-fetch container list so that we don't include removed containers
image_tags_in_use = set(
container['Image'] for container in get_all_containers(client))

images = filter_images_in_use(get_all_images(client), image_tags_in_use)
images = filter_excluded_images(images, exclude_set)

for image_summary in reversed(images):
for image_summary in reversed(list(images)):
remove_image(client, image_summary, max_image_age, dry_run)


def filter_excluded_images(images, exclude_set):
def include_image(image_summary):
image_tags = image_summary.get('RepoTags')
if no_image_tags(image_tags):
return True
return not set(image_tags) & exclude_set

return filter(include_image, images)


def filter_images_in_use(images, image_tags_in_use):
def get_tag_set(image_summary):
image_tags = image_summary.get('RepoTags')
Expand All @@ -87,10 +98,10 @@ def get_tag_set(image_summary):
return set(['%s:latest' % image_summary['Id'][:12]])
return set(image_tags)

def image_is_in_use(image_summary):
def image_not_in_use(image_summary):
return not get_tag_set(image_summary) & image_tags_in_use

return list(filter(image_is_in_use, images))
return filter(image_not_in_use, images)


def is_image_old(image, min_date):
Expand Down Expand Up @@ -140,22 +151,37 @@ def get_tags():
return "%s %s" % (image['Id'][:16], get_tags())


def build_exclude_set(image_tags, exclude_file):
exclude_set = set(image_tags or [])

def is_image_tag(line):
return line and not line.startswith('#')

if exclude_file:
lines = [line.strip() for line in exclude_file.read().split('\n')]
exclude_set.update(filter(is_image_tag, lines))
return exclude_set


def main():
logging.basicConfig(
level=logging.INFO,
format="%(message)s",
stream=sys.stdout)

opts = get_opts()
client = docker.Client(timeout=opts.timeout)
args = get_args()
client = docker.Client(timeout=args.timeout)

if opts.max_container_age:
cleanup_containers(client, opts.max_container_age, opts.dry_run)
if opts.max_image_age:
cleanup_images(client, opts.max_image_age, opts.dry_run)
if args.max_container_age:
cleanup_containers(client, args.max_container_age, args.dry_run)
if args.max_image_age:
exclude_set = build_exclude_set(
args.exclude_image,
args.exclude_image_file)
cleanup_images(client, args.max_image_age, args.dry_run, exclude_set)


def get_opts(args=None):
def get_args(args=None):
parser = argparse.ArgumentParser()
parser.add_argument(
'--max-container-age',
Expand All @@ -175,6 +201,16 @@ def get_opts(args=None):
parser.add_argument(
'-t', '--timeout', type=int, default=60,
help="HTTP timeout in seconds for making docker API calls.")
parser.add_argument(
'--exclude-image',
action='append',
help="Never remove images with this tag.")
parser.add_argument(
'--exclude-image-file',
type=argparse.FileType('r'),
help="Path to a file which contains a list of images to exclude, one "
"image tag per line.")

return parser.parse_args(args=args)


Expand Down
73 changes: 64 additions & 9 deletions tests/docker_gc_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from six import StringIO
import textwrap

import docker.errors
try:
from unittest import mock
Expand Down Expand Up @@ -63,7 +66,7 @@ def test_cleanup_images(mock_client, now):
]
mock_client.inspect_image.side_effect = iter(mock_images)

docker_gc.cleanup_images(mock_client, max_image_age, False)
docker_gc.cleanup_images(mock_client, max_image_age, False, set())
assert mock_client.remove_image.mock_calls == [
mock.call(image['Id']) for image in reversed(images)
]
Expand All @@ -90,7 +93,29 @@ def test_filter_images_in_use():
dict(RepoTags=['new_image:latest', 'new_image:123']),
]
actual = docker_gc.filter_images_in_use(images, image_tags_in_use)
assert actual == expected
assert list(actual) == expected


def test_filter_excluded_images():
exclude_set = set([
'user/one:latest',
'user/foo:latest',
'other:12345',
])
images = [
dict(RepoTags=['<none>:<none>'], Id='babababababaabababab'),
dict(RepoTags=['user/one:latest', 'user/one:abcd']),
dict(RepoTags=['other:abcda']),
dict(RepoTags=['other:12345']),
dict(RepoTags=['new_image:latest', 'new_image:123']),
]
expected = [
dict(RepoTags=['<none>:<none>'], Id='babababababaabababab'),
dict(RepoTags=['other:abcda']),
dict(RepoTags=['new_image:latest', 'new_image:123']),
]
actual = docker_gc.filter_excluded_images(images, exclude_set)
assert list(actual) == expected


def test_is_image_old(image, now):
Expand Down Expand Up @@ -178,20 +203,20 @@ def days_as_seconds(num):
return num * 60 * 60 * 24


def test_get_opts_with_defaults():
opts = docker_gc.get_opts(args=[])
def test_get_args_with_defaults():
opts = docker_gc.get_args(args=[])
assert opts.timeout == 60
assert opts.dry_run is False
assert opts.max_container_age is None
assert opts.max_image_age is None


def test_get_opts_with_args():
def test_get_args_with_args():
with mock.patch(
'docker_custodian.docker_gc.timedelta_type',
autospec=True
) as mock_timedelta_type:
opts = docker_gc.get_opts(args=[
opts = docker_gc.get_args(args=[
'--max-image-age', '30 days',
'--max-container-age', '3d',
])
Expand Down Expand Up @@ -224,16 +249,46 @@ def test_get_all_images(mock_client):
mock_log.info.assert_called_with("Found %s images", count)


def test_build_exclude_set():
image_tags = [
'some_image:latest',
'repo/foo:12345',
'duplicate:latest',
]
exclude_image_file = StringIO(textwrap.dedent("""
# Exclude this one because
duplicate:latest
# Also this one
repo/bar:abab
"""))
expected = set([
'some_image:latest',
'repo/foo:12345',
'duplicate:latest',
'repo/bar:abab',
])

exclude_set = docker_gc.build_exclude_set(image_tags, exclude_image_file)
assert exclude_set == expected


def test_build_exclude_set_empty():
exclude_set = docker_gc.build_exclude_set(None, None)
assert exclude_set == set()


def test_main(mock_client):
with mock.patch(
'docker_custodian.docker_gc.docker.Client',
return_value=mock_client):

with mock.patch(
'docker_custodian.docker_gc.get_opts',
autospec=True) as mock_get_opts:
mock_get_opts.return_value = mock.Mock(
'docker_custodian.docker_gc.get_args',
autospec=True) as mock_get_args:
mock_get_args.return_value = mock.Mock(
max_image_age=100,
max_container_age=200,
exclude_image=[],
exclude_image_file=None,
)
docker_gc.main()

0 comments on commit b45fd97

Please sign in to comment.