From fee79d9e355ff86364d143379ceb0fbf9ad3c8f1 Mon Sep 17 00:00:00 2001 From: milki Date: Wed, 30 Nov 2016 17:00:06 -0800 Subject: [PATCH] Remove dangling volumes --- docker_custodian/docker_gc.py | 33 ++++++++++++++++++++++++++++++ tests/docker_gc_test.py | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/docker_custodian/docker_gc.py b/docker_custodian/docker_gc.py index e2c6c89..9049c92 100644 --- a/docker_custodian/docker_gc.py +++ b/docker_custodian/docker_gc.py @@ -74,6 +74,13 @@ def get_all_images(client): return images +def get_dangling_volumes(client): + log.info("Getting dangling volumes") + volumes = client.volumes({'dangling': True})['Volumes'] + log.info("Found %s dangling volumes", len(volumes)) + return volumes + + 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( @@ -141,6 +148,25 @@ def remove_image(client, image_summary, min_date, dry_run): api_call(client.remove_image, image=image_tag) +def remove_volume(client, volume, dry_run): + if not volume: + return + + log.info("Removing volume %s" % volume['Name']) + if dry_run: + return + + api_call(client.remove_volume, name=volume['Name']) + + +def cleanup_volumes(client, dry_run): + dangling_volumes = get_dangling_volumes(client) + + for volume in reversed(dangling_volumes): + log.info("Removing dangling volume %s", volume['Name']) + remove_volume(client, volume, dry_run) + + def api_call(func, **kwargs): try: return func(**kwargs) @@ -192,6 +218,9 @@ def main(): args.exclude_image_file) cleanup_images(client, args.max_image_age, args.dry_run, exclude_set) + if args.dangling_volumes: + cleanup_volumes(client, args.dry_run) + def get_args(args=None): parser = argparse.ArgumentParser() @@ -207,6 +236,10 @@ def get_args(args=None): help="Maxium age for an image. Images older than this age will be " "removed. Age can be specified in any pytimeparse supported " "format.") + parser.add_argument( + '--dangling-volumes', + action="store_true", + help="Dangling volumes will be removed.") parser.add_argument( '--dry-run', action="store_true", help="Only log actions, don't remove anything.") diff --git a/tests/docker_gc_test.py b/tests/docker_gc_test.py index 221fb11..a7dbfd9 100644 --- a/tests/docker_gc_test.py +++ b/tests/docker_gc_test.py @@ -93,6 +93,32 @@ def test_cleanup_images(mock_client, now): ] +def test_cleanup_volumes(mock_client): + mock_client.volumes.return_value = volumes = { + 'Volumes': [ + { + 'Mountpoint': 'unused', + 'Labels': None, + 'Driver': 'unused', + 'Name': u'one' + }, + { + 'Mountpoint': 'unused', + 'Labels': None, + 'Driver': 'unused', + 'Name': u'two' + }, + ], + 'Warnings': None, + } + + docker_gc.cleanup_volumes(mock_client, False) + assert mock_client.remove_volume.mock_calls == [ + mock.call(name=volume['Name']) + for volume in reversed(volumes['Volumes']) + ] + + def test_filter_images_in_use(): image_tags_in_use = set([ 'user/one:latest', @@ -355,6 +381,18 @@ def test_get_all_images(mock_client): mock_log.info.assert_called_with("Found %s images", count) +def test_get_dangling_volumes(mock_client): + count = 4 + mock_client.volumes.return_value = { + 'Volumes': [mock.Mock() for _ in range(count)] + } + with mock.patch('docker_custodian.docker_gc.log', + autospec=True) as mock_log: + volumes = docker_gc.get_dangling_volumes(mock_client) + assert volumes == mock_client.volumes.return_value['Volumes'] + mock_log.info.assert_called_with("Found %s dangling volumes", count) + + def test_build_exclude_set(): image_tags = [ 'some_image:latest',