diff --git a/dockertidy/GarbageCollector.py b/dockertidy/GarbageCollector.py index 139a5af..b799118 100644 --- a/dockertidy/GarbageCollector.py +++ b/dockertidy/GarbageCollector.py @@ -73,10 +73,7 @@ class GarbageCollector: return containers def include_container(container): - if self._should_exclude_container_with_labels( - container, - config["gc"]["exclude_container_labels"], - ): + if self._should_exclude_container_with_labels(container): return False return True diff --git a/dockertidy/tests/args_test.py b/dockertidy/tests/args_test.py deleted file mode 100644 index 7f12f59..0000000 --- a/dockertidy/tests/args_test.py +++ /dev/null @@ -1,33 +0,0 @@ -import datetime - -from dateutil import tz -from docker_custodian import args - -try: - from unittest import mock -except ImportError: - import mock - - -def test_datetime_seconds_ago(now): - expected = datetime.datetime(2014, 1, 15, 10, 10, tzinfo=tz.tzutc()) - with mock.patch( - 'docker_custodian.args.datetime.datetime', - autospec=True, - ) as mock_datetime: - mock_datetime.now.return_value = now - assert args.datetime_seconds_ago(24 * 60 * 60 * 5) == expected - - -def test_timedelta_type_none(): - assert args.timedelta_type(None) is None - - -def test_timedelta_type(now): - expected = datetime.datetime(2014, 1, 15, 10, 10, tzinfo=tz.tzutc()) - with mock.patch( - 'docker_custodian.args.datetime.datetime', - autospec=True, - ) as mock_datetime: - mock_datetime.now.return_value = now - assert args.timedelta_type('5 days') == expected diff --git a/dockertidy/tests/conftest.py b/dockertidy/tests/conftest.py deleted file mode 100644 index 257375c..0000000 --- a/dockertidy/tests/conftest.py +++ /dev/null @@ -1,54 +0,0 @@ -import datetime - -import docker -import pytest -from dateutil import tz - -try: - from unittest import mock -except ImportError: - import mock - - -@pytest.fixture -def container(): - return { - 'Id': 'abcdabcdabcdabcd', - 'Created': '2013-12-20T17:00:00Z', - 'Name': '/container_name', - 'State': { - 'Running': False, - 'FinishedAt': '2014-01-01T17:30:00Z', - 'StartedAt': '2014-01-01T17:01:00Z', - } - } - - -@pytest.fixture -def image(): - return { - 'Id': 'abcdabcdabcdabcd', - 'Created': '2014-01-20T05:00:00Z', - } - - -@pytest.fixture -def now(): - return datetime.datetime(2014, 1, 20, 10, 10, tzinfo=tz.tzutc()) - - -@pytest.fixture -def earlier_time(): - return datetime.datetime(2014, 1, 1, 0, 0, tzinfo=tz.tzutc()) - - -@pytest.fixture -def later_time(): - return datetime.datetime(2014, 1, 20, 0, 10, tzinfo=tz.tzutc()) - - -@pytest.fixture -def mock_client(): - client = mock.create_autospec(docker.APIClient) - client._version = '1.21' - return client diff --git a/dockertidy/tests/docker_autostop_test.py b/dockertidy/tests/docker_autostop_test.py deleted file mode 100644 index a7e2ae7..0000000 --- a/dockertidy/tests/docker_autostop_test.py +++ /dev/null @@ -1,75 +0,0 @@ -try: - from unittest import mock -except ImportError: - import mock - -from docker_custodian.docker_autostop import build_container_matcher -from docker_custodian.docker_autostop import get_opts -from docker_custodian.docker_autostop import has_been_running_since -from docker_custodian.docker_autostop import main -from docker_custodian.docker_autostop import stop_container -from docker_custodian.docker_autostop import stop_containers - - -def test_stop_containers(mock_client, container, now): - matcher = mock.Mock() - mock_client.containers.return_value = [container] - mock_client.inspect_container.return_value = container - - stop_containers(mock_client, now, matcher, False) - matcher.assert_called_once_with('container_name') - mock_client.stop.assert_called_once_with(container['Id']) - - -def test_stop_container(mock_client): - id = 'asdb' - stop_container(mock_client, id) - mock_client.stop.assert_called_once_with(id) - - -def test_build_container_matcher(): - prefixes = ['one_', 'two_'] - matcher = build_container_matcher(prefixes) - - assert matcher('one_container') - assert matcher('two_container') - assert not matcher('three_container') - assert not matcher('one') - - -def test_has_been_running_since_true(container, later_time): - assert has_been_running_since(container, later_time) - - -def test_has_been_running_since_false(container, earlier_time): - assert not has_been_running_since(container, earlier_time) - - -@mock.patch('docker_custodian.docker_autostop.build_container_matcher', autospec=True) -@mock.patch('docker_custodian.docker_autostop.stop_containers', autospec=True) -@mock.patch('docker_custodian.docker_autostop.get_opts', autospec=True) -@mock.patch('docker_custodian.docker_autostop.docker', autospec=True) -def test_main(mock_docker, mock_get_opts, mock_stop_containers, mock_build_matcher): - mock_get_opts.return_value.timeout = 30 - main() - mock_get_opts.assert_called_once_with() - mock_build_matcher.assert_called_once_with(mock_get_opts.return_value.prefix) - mock_stop_containers.assert_called_once_with(mock.ANY, mock_get_opts.return_value.max_run_time, - mock_build_matcher.return_value, - mock_get_opts.return_value.dry_run) - - -def test_get_opts_with_defaults(): - opts = get_opts(args=['--prefix', 'one', '--prefix', 'two']) - assert opts.timeout == 60 - assert opts.dry_run is False - assert opts.prefix == ['one', 'two'] - assert opts.max_run_time is None - - -def test_get_opts_with_args(now): - with mock.patch('docker_custodian.docker_autostop.timedelta_type', - autospec=True) as mock_timedelta_type: - opts = get_opts(args=['--prefix', 'one', '--max-run-time', '24h']) - assert opts.max_run_time == mock_timedelta_type.return_value - mock_timedelta_type.assert_called_once_with('24h') diff --git a/dockertidy/tests/docker_gc_test.py b/dockertidy/tests/docker_gc_test.py deleted file mode 100644 index e171276..0000000 --- a/dockertidy/tests/docker_gc_test.py +++ /dev/null @@ -1,545 +0,0 @@ -import textwrap -from io import StringIO - -import docker.errors -import requests.exceptions -from docker_custodian import docker_gc - -try: - from unittest import mock -except ImportError: - import mock - - -class TestShouldRemoveContainer(object): - - def test_is_running(self, container, now): - container['State']['Running'] = True - assert not docker_gc.should_remove_container(container, now) - - def test_is_ghost(self, container, now): - container['State']['Ghost'] = True - assert docker_gc.should_remove_container(container, now) - - def test_old_never_run(self, container, now, earlier_time): - container['Created'] = str(earlier_time) - container['State']['FinishedAt'] = docker_gc.YEAR_ZERO - assert docker_gc.should_remove_container(container, now) - - def test_not_old_never_run(self, container, now, earlier_time): - container['Created'] = str(now) - container['State']['FinishedAt'] = docker_gc.YEAR_ZERO - assert not docker_gc.should_remove_container(container, now) - - def test_old_stopped(self, 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.should_remove_container(container, now) - - -def test_cleanup_containers(mock_client, now): - max_container_age = now - mock_client.containers.return_value = [ - { - 'Id': 'abcd' - }, - { - 'Id': 'abbb' - }, - ] - mock_containers = [ - { - 'Id': 'abcd', - 'Name': 'one', - 'State': { - 'Running': False, - 'FinishedAt': '2014-01-01T01:01:01Z', - }, - }, - { - 'Id': 'abbb', - 'Name': 'two', - 'State': { - '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, None) - mock_client.remove_container.assert_called_once_with(container='abcd', v=True) - - -def test_filter_excluded_containers(): - mock_containers = [ - { - 'Labels': { - 'toot': '' - } - }, - { - 'Labels': { - 'too': 'lol' - } - }, - { - 'Labels': { - 'toots': 'lol' - } - }, - { - 'Labels': { - 'foo': 'bar' - } - }, - { - 'Labels': None - }, - ] - result = docker_gc.filter_excluded_containers(mock_containers, None) - assert mock_containers == list(result) - exclude_labels = [ - docker_gc.ExcludeLabel(key='too', value=None), - docker_gc.ExcludeLabel(key='foo', value=None), - ] - result = docker_gc.filter_excluded_containers( - mock_containers, - exclude_labels, - ) - assert [mock_containers[0], mock_containers[2], mock_containers[4]] == list(result) - exclude_labels = [ - docker_gc.ExcludeLabel(key='too*', value='lol'), - ] - result = docker_gc.filter_excluded_containers( - mock_containers, - exclude_labels, - ) - assert [mock_containers[0], mock_containers[3], mock_containers[4]] == list(result) - - -def test_cleanup_images(mock_client, now): - max_image_age = now - mock_client.images.return_value = images = [ - { - 'Id': 'abcd' - }, - { - 'Id': 'abbb' - }, - ] - mock_images = [ - { - 'Id': 'abcd', - 'Created': '2014-01-01T01:01:01Z' - }, - { - 'Id': 'abbb', - 'Created': '2014-01-01T01:01:01Z' - }, - ] - mock_client.inspect_image.side_effect = iter(mock_images) - - docker_gc.cleanup_images(mock_client, max_image_age, False, set()) - assert mock_client.remove_image.mock_calls == [ - mock.call(image=image['Id']) for image in reversed(images) - ] - - -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', - 'user/foo:latest', - 'other:12345', - '2471708c19be:latest', - ]) - images = [ - { - 'RepoTags': [':'], - 'Id': '2471708c19beabababab' - }, - { - 'RepoTags': [':'], - 'Id': 'babababababaabababab' - }, - { - 'RepoTags': ['user/one:latest', 'user/one:abcd'] - }, - { - 'RepoTags': ['other:abcda'] - }, - { - 'RepoTags': ['other:12345'] - }, - { - 'RepoTags': ['new_image:latest', 'new_image:123'] - }, - ] - expected = [ - { - 'RepoTags': [':'], - 'Id': 'babababababaabababab' - }, - { - 'RepoTags': ['other:abcda'] - }, - { - 'RepoTags': ['new_image:latest', 'new_image:123'] - }, - ] - actual = docker_gc.filter_images_in_use(images, image_tags_in_use) - assert list(actual) == expected - - -def test_filter_images_in_use_by_id(mock_client, now): - mock_client._version = '1.21' - mock_client.containers.return_value = [ - { - 'Id': 'abcd', - 'ImageID': '1' - }, - { - 'Id': 'abbb', - 'ImageID': '2' - }, - ] - mock_containers = [{ - 'Id': 'abcd', - 'Name': 'one', - 'State': { - 'Running': False, - 'FinishedAt': '2014-01-01T01:01:01Z' - } - }, { - 'Id': 'abbb', - 'Name': 'two', - 'State': { - 'Running': True, - 'FinishedAt': '2014-01-01T01:01:01Z' - } - }] - mock_client.inspect_container.side_effect = iter(mock_containers) - mock_client.images.return_value = [ - { - 'Id': '1', - 'Created': '2014-01-01T01:01:01Z' - }, - { - 'Id': '2', - 'Created': '2014-01-01T01:01:01Z' - }, - { - 'Id': '3', - 'Created': '2014-01-01T01:01:01Z' - }, - { - 'Id': '4', - 'Created': '2014-01-01T01:01:01Z' - }, - { - 'Id': '5', - 'Created': '2014-01-01T01:01:01Z' - }, - { - 'Id': '6', - 'Created': '2014-01-01T01:01:01Z' - }, - ] - mock_client.inspect_image.side_effect = lambda image: { - 'Id': image, - 'Created': '2014-01-01T01:01:01Z' - } - docker_gc.cleanup_images(mock_client, now, False, set()) - assert mock_client.remove_image.mock_calls == [ - mock.call(image=id_) for id_ in ['6', '5', '4', '3'] - ] - - -def test_filter_excluded_images(): - exclude_set = set([ - 'user/one:latest', - 'user/foo:latest', - 'other:12345', - ]) - images = [ - { - 'RepoTags': [':'], - 'Id': 'babababababaabababab' - }, - { - 'RepoTags': ['user/one:latest', 'user/one:abcd'] - }, - { - 'RepoTags': ['other:abcda'] - }, - { - 'RepoTags': ['other:12345'] - }, - { - 'RepoTags': ['new_image:latest', 'new_image:123'] - }, - ] - expected = [ - { - 'RepoTags': [':'], - 'Id': 'babababababaabababab' - }, - { - 'RepoTags': ['other:abcda'] - }, - { - 'RepoTags': ['new_image:latest', 'new_image:123'] - }, - ] - actual = docker_gc.filter_excluded_images(images, exclude_set) - assert list(actual) == expected - - -def test_filter_excluded_images_advanced(): - exclude_set = set([ - 'user/one:*', - 'user/foo:tag*', - 'user/repo-*:tag', - ]) - images = [ - { - 'RepoTags': [':'], - 'Id': 'babababababaabababab' - }, - { - 'RepoTags': ['user/one:latest', 'user/one:abcd'] - }, - { - 'RepoTags': ['user/foo:test'] - }, - { - 'RepoTags': ['user/foo:tag123'] - }, - { - 'RepoTags': ['user/repo-1:tag'] - }, - { - 'RepoTags': ['user/repo-2:tag'] - }, - ] - expected = [ - { - 'RepoTags': [':'], - 'Id': 'babababababaabababab' - }, - { - 'RepoTags': ['user/foo:test'], - }, - ] - actual = docker_gc.filter_excluded_images(images, exclude_set) - assert list(actual) == expected - - -def test_is_image_old(image, now): - assert docker_gc.is_image_old(image, now) - - -def test_is_image_old_false(image, later_time): - assert not docker_gc.is_image_old(image, later_time) - - -def test_remove_image_no_tags(mock_client, image, now): - image_id = 'abcd' - image_summary = {'Id': image_id} - mock_client.inspect_image.return_value = image - docker_gc.remove_image(mock_client, image_summary, now, False) - - mock_client.remove_image.assert_called_once_with(image=image_id) - - -def test_remove_image_new_image_not_removed(mock_client, image, later_time): - image_id = 'abcd' - image_summary = {'Id': image_id} - mock_client.inspect_image.return_value = image - docker_gc.remove_image(mock_client, image_summary, later_time, False) - - assert not mock_client.remove_image.mock_calls - - -def test_remove_image_with_tags(mock_client, image, now): - image_id = 'abcd' - repo_tags = ['user/one:latest', 'user/one:12345'] - image_summary = {'Id': image_id, 'RepoTags': repo_tags} - mock_client.inspect_image.return_value = image - docker_gc.remove_image(mock_client, image_summary, now, False) - - assert mock_client.remove_image.mock_calls == [mock.call(image=tag) for tag in repo_tags] - - -def test_api_call_success(): - func = mock.Mock() - container = "abcd" - result = docker_gc.api_call(func, container=container) - func.assert_called_once_with(container="abcd") - assert result == func.return_value - - -def test_api_call_with_timeout(): - func = mock.Mock(side_effect=requests.exceptions.ReadTimeout("msg"), __name__="remove_image") - image = "abcd" - - with mock.patch('docker_custodian.docker_gc.log', autospec=True) as mock_log: - docker_gc.api_call(func, image=image) - - func.assert_called_once_with(image="abcd") - mock_log.warn.assert_called_once_with('Failed to call remove_image ' + 'image=abcd msg') - - -def test_api_call_with_api_error(): - func = mock.Mock(side_effect=docker.errors.APIError("Ooops", - mock.Mock(status_code=409, - reason="Conflict"), - explanation="failed"), - __name__="remove_image") - image = "abcd" - - with mock.patch('docker_custodian.docker_gc.log', autospec=True) as mock_log: - docker_gc.api_call(func, image=image) - - func.assert_called_once_with(image="abcd") - mock_log.warn.assert_called_once_with('Error calling remove_image image=abcd ' - '409 Client Error: Conflict ("failed")') - - -def days_as_seconds(num): - return num * 60 * 60 * 24 - - -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_args_with_args(): - with mock.patch('docker_custodian.docker_gc.timedelta_type', - autospec=True) as mock_timedelta_type: - opts = docker_gc.get_args(args=[ - '--max-image-age', - '30 days', - '--max-container-age', - '3d', - ]) - assert mock_timedelta_type.mock_calls == [ - mock.call('30 days'), - mock.call('3d'), - ] - assert opts.max_container_age == mock_timedelta_type.return_value - assert opts.max_image_age == mock_timedelta_type.return_value - - -def test_get_all_containers(mock_client): - count = 10 - mock_client.containers.return_value = [mock.Mock() for _ in range(count)] - with mock.patch('docker_custodian.docker_gc.log', autospec=True) as mock_log: - containers = docker_gc.get_all_containers(mock_client) - assert containers == mock_client.containers.return_value - mock_client.containers.assert_called_once_with(all=True) - mock_log.info.assert_called_with("Found %s containers", count) - - -def test_get_all_images(mock_client): - count = 7 - mock_client.images.return_value = [mock.Mock() for _ in range(count)] - with mock.patch('docker_custodian.docker_gc.log', autospec=True) as mock_log: - images = docker_gc.get_all_images(mock_client) - assert images == mock_client.images.return_value - 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', - '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_format_exclude_labels(): - exclude_label_args = [ - 'voo*', - 'doo=poo', - ] - expected = [ - docker_gc.ExcludeLabel(key='voo*', value=None), - docker_gc.ExcludeLabel(key='doo', value='poo'), - ] - exclude_labels = docker_gc.format_exclude_labels(exclude_label_args) - assert expected == exclude_labels - - -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.APIClient', return_value=mock_client): - - with mock.patch('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, - exclude_container_label=[], - ) - docker_gc.main() diff --git a/dockertidy/tests/fixtures/__init__.py b/dockertidy/tests/fixtures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dockertidy/tests/fixtures/fixtures.py b/dockertidy/tests/fixtures/fixtures.py new file mode 100644 index 0000000..bbdc19e --- /dev/null +++ b/dockertidy/tests/fixtures/fixtures.py @@ -0,0 +1,92 @@ +"""Global pytest fixtures.""" +import datetime + +import pytest +from dateutil import tz + + +@pytest.fixture +def container(): + return { + "Id": "abcdabcdabcdabcd", + "Created": "2013-12-20T17:00:00Z", + "Name": "/container_name", + "State": { + "Running": False, + "FinishedAt": "2014-01-01T17:30:00Z", + "StartedAt": "2014-01-01T17:01:00Z", + } + } + + +@pytest.fixture +def containers(): + return [ + { + "Id": "abcd", + "Name": "one", + "Created": "2014-01-01T01:01:01Z", + "State": { + "Running": False, + "FinishedAt": "2014-01-01T01:01:01Z", + }, + }, + { + "Id": "abbb", + "Name": "two", + "Created": "2014-01-01T01:01:01Z", + "State": { + "Running": True, + "FinishedAt": "2014-01-01T01:01:01Z", + }, + }, + ] + + +@pytest.fixture +def image(): + return { + "Id": "abcdabcdabcdabcd", + "Created": "2014-01-20T05:00:00Z", + } + + +@pytest.fixture +def images(): + return [ + { + "RepoTags": [":"], + "Id": "2471708c19beabababab" + }, + { + "RepoTags": [":"], + "Id": "babababababaabababab" + }, + { + "RepoTags": ["user/one:latest", "user/one:abcd"] + }, + { + "RepoTags": ["other-1:abcda"] + }, + { + "RepoTags": ["other-2:abc45"] + }, + { + "RepoTags": ["new_image:latest", "new_image:123"] + }, + ] + + +@pytest.fixture +def now(): + return datetime.datetime(2014, 1, 20, 10, 10, tzinfo=tz.tzutc()) + + +@pytest.fixture +def earlier_time(): + return datetime.datetime(2014, 1, 1, 0, 0, tzinfo=tz.tzutc()) + + +@pytest.fixture +def later_time(): + return datetime.datetime(2014, 1, 20, 0, 10, tzinfo=tz.tzutc()) diff --git a/dockertidy/tests/unit/test_autostop.py b/dockertidy/tests/unit/test_autostop.py new file mode 100644 index 0000000..51ef67f --- /dev/null +++ b/dockertidy/tests/unit/test_autostop.py @@ -0,0 +1,42 @@ +"""Test Autostop class.""" + +import docker +import pytest + +from dockertidy import Autostop + +pytest_plugins = [ + "dockertidy.tests.fixtures.fixtures", +] + + +@pytest.fixture +def autostop(): + stop = Autostop.AutoStop() + return stop + + +def test_stop_container(autostop, mocker): + client = mocker.create_autospec(docker.APIClient) + cid = "asdb" + + autostop._stop_container(client, cid) + client.stop.assert_called_once_with(cid) + + +def test_build_container_matcher(autostop, mocker): + prefixes = ["one_", "two_"] + matcher = autostop._build_container_matcher(prefixes) + + assert matcher("one_container") + assert matcher("two_container") + assert not matcher("three_container") + assert not matcher("one") + + +def test_has_been_running_since_true(autostop, container, later_time): + assert autostop._has_been_running_since(container, later_time) + + +def test_has_been_running_since_false(autostop, container, earlier_time): + assert not autostop._has_been_running_since(container, earlier_time) diff --git a/dockertidy/tests/unit/test_garbagecollector.py b/dockertidy/tests/unit/test_garbagecollector.py new file mode 100644 index 0000000..f5a5c69 --- /dev/null +++ b/dockertidy/tests/unit/test_garbagecollector.py @@ -0,0 +1,449 @@ +"""Test GarbageCollector class.""" + +import docker +import pytest +import requests + +from dockertidy import GarbageCollector + +pytest_plugins = [ + "dockertidy.tests.fixtures.fixtures", +] + + +@pytest.fixture +def gc(): + gc = GarbageCollector.GarbageCollector() + return gc + + +def test_is_running(gc, container, now): + container["State"]["Running"] = True + + assert not gc._should_remove_container(container, now) + + +def test_is_ghost(gc, container, now): + container["State"]["Ghost"] = True + + assert gc._should_remove_container(container, now) + + +def test_old_never_run(gc, container, now, earlier_time): + container["Created"] = str(earlier_time) + container["State"]["FinishedAt"] = gc.YEAR_ZERO + + assert gc._should_remove_container(container, now) + + +def test_not_old_never_run(gc, container, now, earlier_time): + container["Created"] = str(now) + container["State"]["FinishedAt"] = gc.YEAR_ZERO + + assert not gc._should_remove_container(container, now) + + +def test_old_stopped(gc, container, now): + assert gc._should_remove_container(container, now) + + +def test_not_old(gc, container, now): + container["State"]["FinishedAt"] = "2014-01-21T00:00:00Z" + + assert not gc._should_remove_container(container, now) + + +def test_cleanup_containers(gc, mocker, containers): + client = mocker.create_autospec(docker.APIClient) + client.containers.return_value = [ + { + "Id": "abcd" + }, + { + "Id": "abbb" + }, + ] + client.inspect_container.side_effect = iter(containers) + + gc.config.config["gc"]["max_container_age"] = "0day" + gc.docker = client + gc.cleanup_containers() + client.remove_container.assert_called_once_with(container="abcd", v=True) + + +def test_filter_excluded_containers(gc): + containers = [ + { + "Labels": { + "toot": "" + } + }, + { + "Labels": { + "too": "lol" + } + }, + { + "Labels": { + "toots": "lol" + } + }, + { + "Labels": { + "foo": "bar" + } + }, + { + "Labels": None + }, + ] + + result = gc._filter_excluded_containers(containers) + assert containers == list(result) + + gc.config.config["gc"]["exclude_container_labels"] = [ + gc.ExcludeLabel(key="too", value=None), + gc.ExcludeLabel(key="foo", value=None), + ] + result = gc._filter_excluded_containers(containers) + assert [containers[0], containers[2], containers[4]] == list(result) + + gc.config.config["gc"]["exclude_container_labels"] = [ + gc.ExcludeLabel(key="too*", value="lol"), + ] + result = gc._filter_excluded_containers(containers) + assert [containers[0], containers[3], containers[4]] == list(result) + + +def test_cleanup_images(mocker, gc, containers): + client = mocker.create_autospec(docker.APIClient) + client._version = "1.21" + client.images.return_value = images = [ + { + "Id": "abcd" + }, + { + "Id": "abbb" + }, + ] + client.inspect_image.side_effect = iter(containers) + + gc.docker = client + gc.config.config["gc"]["max_image_age"] = "0days" + gc.cleanup_images(client) + assert client.remove_image.mock_calls == [ + mocker.call(image=image["Id"]) for image in reversed(images) + ] + + +def test_cleanup_volumes(mocker, gc): + client = mocker.create_autospec(docker.APIClient) + 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, + } + + gc.docker = client + gc.cleanup_volumes() + assert client.remove_volume.mock_calls == [ + mocker.call(name=volume["Name"]) for volume in reversed(volumes["Volumes"]) + ] + + +def test_filter_images_in_use(gc, images): + image_tags_in_use = set([ + "user/one:latest", + "user/foo:latest", + "other-2:abc45", + "2471708c19be:latest", + ]) + expected = [ + { + "RepoTags": [":"], + "Id": "babababababaabababab" + }, + { + "RepoTags": ["other-1:abcda"] + }, + { + "RepoTags": ["new_image:latest", "new_image:123"] + }, + ] + + actual = gc._filter_images_in_use(images, image_tags_in_use) + assert list(actual) == expected + + +def test_filter_images_in_use_by_id(mocker, gc, containers): + client = mocker.create_autospec(docker.APIClient) + client._version = "1.21" + client.containers.return_value = [ + { + "Id": "abcd", + "ImageID": "1" + }, + { + "Id": "abbb", + "ImageID": "2" + }, + ] + + client.inspect_container.side_effect = iter(containers) + client.images.return_value = [ + { + "Id": "1", + "Created": "2014-01-01T01:01:01Z" + }, + { + "Id": "2", + "Created": "2014-01-01T01:01:01Z" + }, + { + "Id": "3", + "Created": "2014-01-01T01:01:01Z" + }, + { + "Id": "4", + "Created": "2014-01-01T01:01:01Z" + }, + { + "Id": "5", + "Created": "2014-01-01T01:01:01Z" + }, + { + "Id": "6", + "Created": "2014-01-01T01:01:01Z" + }, + ] + + client.inspect_image.side_effect = lambda image: { + "Id": image, + "Created": "2014-01-01T01:01:01Z" + } + + gc.docker = client + gc.config.config["gc"]["max_image_age"] = "0days" + gc.cleanup_images(set()) + assert client.remove_image.mock_calls == [ + mocker.call(image=id_) for id_ in ["6", "5", "4", "3"] + ] + + +def test_filter_excluded_images(gc, images): + exclude_set = set([ + "user/one:latest", + "user/foo:latest", + "other-2:abc45", + ]) + expected = [ + { + "RepoTags": [":"], + "Id": "2471708c19beabababab" + }, + { + "RepoTags": [":"], + "Id": "babababababaabababab" + }, + { + "RepoTags": ["other-1:abcda"] + }, + { + "RepoTags": ["new_image:latest", "new_image:123"] + }, + ] + + actual = gc._filter_excluded_images(images, exclude_set) + assert list(actual) == expected + + +def test_filter_excluded_images_advanced(gc, images): + exclude_set = set([ + "user/one:*", + "new_*:123", + "other-1:abc*", + ]) + expected = [ + { + "RepoTags": [":"], + "Id": "2471708c19beabababab" + }, + { + "RepoTags": [":"], + "Id": "babababababaabababab" + }, + { + "RepoTags": ["other-2:abc45"] + }, + ] + + actual = gc._filter_excluded_images(images, exclude_set) + assert list(actual) == expected + + +def test_is_image_old(gc, image, now): + assert gc._is_image_old(image, now) + + +def test_is_image_old_false(gc, image, later_time): + assert not gc._is_image_old(image, later_time) + + +def test_remove_image_no_tags(mocker, gc, image, now): + client = mocker.create_autospec(docker.APIClient) + image_id = "abcd" + image_summary = {"Id": image_id} + client.inspect_image.return_value = image + + gc.docker = client + gc._remove_image(image_summary, now) + client.remove_image.assert_called_once_with(image=image_id) + + +def test_remove_image_new_image_not_removed(mocker, gc, image, later_time): + client = mocker.create_autospec(docker.APIClient) + image_id = "abcd" + image_summary = {"Id": image_id} + client.inspect_image.return_value = image + + gc.docker = client + gc._remove_image(image_summary, later_time) + assert not client.remove_image.mock_calls + + +def test_remove_image_with_tags(mocker, gc, image, now): + client = mocker.create_autospec(docker.APIClient) + image_id = "abcd" + repo_tags = ["user/one:latest", "user/one:12345"] + image_summary = {"Id": image_id, "RepoTags": repo_tags} + client.inspect_image.return_value = image + + gc.docker = client + gc._remove_image(image_summary, now) + assert client.remove_image.mock_calls == [mocker.call(image=tag) for tag in repo_tags] + + +def test_api_call_success(mocker, gc): + func = mocker.Mock() + container = "abcd" + result = gc._api_call(func, container=container) + func.assert_called_once_with(container="abcd") + + assert result == func.return_value + + +def test_api_call_with_timeout(mocker, gc): + func = mocker.Mock(side_effect=requests.exceptions.ReadTimeout("msg"), __name__="remove_image") + image = "abcd" + + mock_log = mocker.patch.object(gc, "logger", autospec=True) + gc._api_call(func, image=image) + + func.assert_called_once_with(image="abcd") + mock_log.warn.assert_called_once_with("Failed to call remove_image " + "image=abcd msg") + + +def test_api_call_with_api_error(mocker, gc): + func = mocker.Mock( + side_effect=docker.errors.APIError( + "Ooops", mocker.Mock(status_code=409, reason="Conflict"), explanation="failed" + ), + __name__="remove_image" + ) + image = "abcd" + + mock_log = mocker.patch.object(gc, "logger", autospec=True) + gc._api_call(func, image=image) + + func.assert_called_once_with(image="abcd") + mock_log.warn.assert_called_once_with( + "Error calling remove_image image=abcd " + '409 Client Error: Conflict ("failed")' + ) + + +def test_get_all_containers(mocker, gc): + client = mocker.create_autospec(docker.APIClient) + count = 10 + client.containers.return_value = [mocker.Mock() for _ in range(count)] + + mock_log = mocker.patch.object(gc, "logger", autospec=True) + + gc.docker = client + containers = gc._get_all_containers() + assert containers == client.containers.return_value + client.containers.assert_called_once_with(all=True) + mock_log.info.assert_called_with("Found %s containers", count) + + +def test_get_all_images(mocker, gc): + client = mocker.create_autospec(docker.APIClient) + count = 7 + client.images.return_value = [mocker.Mock() for _ in range(count)] + + mock_log = mocker.patch.object(gc, "logger", autospec=True) + + gc.docker = client + images = gc._get_all_images() + assert images == client.images.return_value + mock_log.info.assert_called_with("Found %s images", count) + + +def test_get_dangling_volumes(mocker, gc): + client = mocker.create_autospec(docker.APIClient) + count = 4 + client.volumes.return_value = {"Volumes": [mocker.Mock() for _ in range(count)]} + + mock_log = mocker.patch.object(gc, "logger", autospec=True) + + gc.docker = client + volumes = gc._get_dangling_volumes() + assert volumes == client.volumes.return_value["Volumes"] + mock_log.info.assert_called_with("Found %s dangling volumes", count) + + +def test_build_exclude_set(gc): + gc.config.config["gc"]["exclude_images"] = [ + "some_image:latest", + "repo/foo:12345", + "duplicate:latest", + ] + expected = set([ + "some_image:latest", + "repo/foo:12345", + "duplicate:latest", + ]) + + exclude_set = gc._build_exclude_set() + assert exclude_set == expected + + +def test_format_exclude_labels(gc): + gc.config.config["gc"]["exclude_container_label"] = [ + "voo*", + "doo=poo", + ] + expected = [ + gc.ExcludeLabel(key="voo*", value=None), + gc.ExcludeLabel(key="doo", value="poo"), + ] + exclude_labels = gc._format_exclude_labels() + assert expected == exclude_labels + + +def test_build_exclude_set_empty(gc): + gc.config.config["gc"]["exclude_images"] = [] + exclude_set = gc._build_exclude_set() + assert exclude_set == set()