diff --git a/docker_custodian/args.py b/docker_custodian/args.py index 674266d..a664388 100644 --- a/docker_custodian/args.py +++ b/docker_custodian/args.py @@ -5,7 +5,7 @@ from pytimeparse import timeparse def timedelta_type(value): - """Retrun the :class:`datetime.datetime.DateTime` for a time in the past. + """Return the :class:`datetime.datetime.DateTime` for a time in the past. :param value: a string containing a time format supported by :mod:`pytimeparse` """ diff --git a/docker_custodian/docker_gc.py b/docker_custodian/docker_gc.py index 121096d..e7a1ac9 100644 --- a/docker_custodian/docker_gc.py +++ b/docker_custodian/docker_gc.py @@ -21,12 +21,14 @@ log = logging.getLogger(__name__) YEAR_ZERO = "0001-01-01T00:00:00Z" -def cleanup_containers(client, max_container_age, dry_run): +def cleanup_containers(client, max_container_age, min_container_age, dry_run): all_containers = get_all_containers(client) for container_summary in reversed(all_containers): container = api_call(client.inspect_container, container_summary['Id']) - if not container or not is_container_old(container, max_container_age): + if not container or not should_remove_container(container, + max_container_age, + min_container_age): continue log.info("Removing container %s %s %s" % ( @@ -38,7 +40,7 @@ def cleanup_containers(client, max_container_age, dry_run): api_call(client.remove_container, container['Id']) -def is_container_old(container, min_date): +def should_remove_container(container, min_date, max_date=None): state = container.get('State', {}) if state.get('Running'): return False @@ -46,16 +48,21 @@ def is_container_old(container, min_date): if state.get('Ghost'): return True + created_date = dateutil.parser.parse(container['Created']) + + # Don't delete recently created containers + if max_date and created_date > max_date: + return False + # Container was created, but never used - if (state.get('FinishedAt') == YEAR_ZERO and - dateutil.parser.parse(container['Created']) < min_date): + if state.get('FinishedAt') == YEAR_ZERO and created_date < min_date: return True return dateutil.parser.parse(state['FinishedAt']) < min_date def get_all_containers(client): - log.info("Getting all continers") + log.info("Getting all containers") containers = client.containers(all=True) log.info("Found %s containers", len(containers)) return containers @@ -173,7 +180,8 @@ def main(): client = docker.Client(version='auto', timeout=args.timeout) if args.max_container_age: - cleanup_containers(client, args.max_container_age, args.dry_run) + cleanup_containers(client, args.max_container_age, + args.min_container_age, args.dry_run) if args.max_image_age: exclude_set = build_exclude_set( args.exclude_image, @@ -189,6 +197,12 @@ def get_args(args=None): help="Maximum age for a container. Containers older than this age " "will be removed. Age can be specified in any pytimeparse " "supported format.") + parser.add_argument( + '--min-container-age', + type=timedelta_type, + help="Minimum age for a container when --max-container-age is used. " + "Containers younger than this age will not be removed. Age can " + "be specified in any pytimeparse supported format.") parser.add_argument( '--max-image-age', type=timedelta_type, diff --git a/tests/docker_gc_test.py b/tests/docker_gc_test.py index 0081a4a..eecc4a4 100644 --- a/tests/docker_gc_test.py +++ b/tests/docker_gc_test.py @@ -11,26 +11,36 @@ import requests.exceptions from docker_custodian import docker_gc -class TestIsOldContainer(object): +class TestShouldRemoveContainer(object): def test_is_running(self, container, now): container['State']['Running'] = True - assert not docker_gc.is_container_old(container, now) + assert not docker_gc.should_remove_container(container, now) def test_is_ghost(self, container, now): container['State']['Ghost'] = True - assert docker_gc.is_container_old(container, now) + assert docker_gc.should_remove_container(container, now) def test_old_never_run(self, container, now): container['State']['FinishedAt'] = docker_gc.YEAR_ZERO - assert docker_gc.is_container_old(container, now) + assert docker_gc.should_remove_container(container, now) def test_old_stopped(self, container, now): - assert docker_gc.is_container_old(container, now) + assert docker_gc.should_remove_container(container, now) def test_not_old(self, container, now): container['State']['FinishedAt'] = '2014-01-21T00:00:00Z' - assert not docker_gc.is_container_old(container, now) + assert not docker_gc.should_remove_container(container, now) + + def test_recently_created(self, container, now, earlier_time, later_time): + container['Created'] = str(later_time) + assert not docker_gc.should_remove_container(container, earlier_time, + now) + + def test_not_recently_created(self, container, now, earlier_time, + later_time): + container['Created'] = str(earlier_time) + assert docker_gc.should_remove_container(container, now, later_time) def test_cleanup_containers(mock_client, now): @@ -43,14 +53,16 @@ def test_cleanup_containers(mock_client, now): dict( Id='abcd', Name='one', + Created=str(now), State=dict(Running=False, FinishedAt='2014-01-01T01:01:01Z')), dict( Id='abbb', Name='two', + Created=str(now), State=dict(Running=True, FinishedAt='2014-01-01T01:01:01Z')) ] mock_client.inspect_container.side_effect = iter(mock_containers) - docker_gc.cleanup_containers(mock_client, max_container_age, False) + docker_gc.cleanup_containers(mock_client, max_container_age, None, False) mock_client.remove_container.assert_called_once_with('abcd')