7444: Delete containers as soon as they stop.
authorTom Clegg <tom@curoverse.com>
Fri, 30 Oct 2015 22:18:09 +0000 (18:18 -0400)
committerTom Clegg <tom@curoverse.com>
Fri, 30 Oct 2015 22:18:09 +0000 (18:18 -0400)
services/dockercleaner/arvados_docker/cleaner.py
services/dockercleaner/tests/test_cleaner.py

index 191cb55601053d1f58a284ce08847149d04d2522..89d65216dc52f600b6ccd52aaa7c65aa5203ee67 100755 (executable)
@@ -177,9 +177,10 @@ class DockerImageUseRecorder(DockerEventListener):
 class DockerImageCleaner(DockerImageUseRecorder):
     event_handlers = DockerImageUseRecorder.event_handlers.copy()
 
-    def __init__(self, images, docker_client, events):
+    def __init__(self, images, docker_client, events, remove_stopped_containers=False):
         super().__init__(images, docker_client, events)
         self.logged_unknown = set()
+        self.remove_stopped_containers = remove_stopped_containers
 
     def new_container(self, event, container_hash):
         container_image_id = container_hash['Image']
@@ -188,6 +189,18 @@ class DockerImageCleaner(DockerImageUseRecorder):
             self.images.add_image(image_hash)
         return super().new_container(event, container_hash)
 
+    @event_handlers.on('die')
+    def clean_container(self, event=None):
+        if not self.remove_stopped_containers:
+            return
+        cid = event['id']
+        try:
+            self.docker_client.remove_container(cid)
+        except docker.errors.APIError as error:
+            logger.warning("Failed to remove container %s: %s", cid, error)
+        else:
+            logger.info("Removed container %s", cid)
+
     @event_handlers.on('destroy')
     def clean_images(self, event=None):
         for image_id in self.images.should_delete():
@@ -225,6 +238,10 @@ def parse_arguments(arguments):
     parser.add_argument(
         '--quota', action='store', type=human_size, required=True,
         help="space allowance for Docker images, suffixed with K/M/G/T")
+    parser.add_argument(
+        '--no-remove-stopped-containers', action='store_false', default=True,
+        dest='remove_stopped_containers',
+        help="do not remove containers (default: remove on exit)")
     parser.add_argument(
         '--verbose', '-v', action='count', default=0,
         help="log more information")
@@ -246,7 +263,8 @@ def run(args, docker_client):
         images, docker_client, docker_client.events(since=1, until=start_time))
     use_recorder.run()
     cleaner = DockerImageCleaner(
-        images, docker_client, docker_client.events(since=start_time))
+        images, docker_client, docker_client.events(since=start_time),
+        remove_stopped_containers=args.remove_stopped_containers)
     logger.info("Starting cleanup loop")
     cleaner.clean_images()
     cleaner.run()
index fd959de7762dd2b0a54f5b08b1107e4aedc45d5a..52efabc8fea390d118fb0dcbe739fcd24ab60eb4 100644 (file)
@@ -354,3 +354,23 @@ class RunTestCase(unittest.TestCase):
         self.assertLessEqual(test_start_time, event_kwargs[0]['until'])
         self.assertIn('since', event_kwargs[1])
         self.assertEqual(event_kwargs[0]['until'], event_kwargs[1]['since'])
+
+
+class ContainerRemovalTestCase(unittest.TestCase):
+    def setUp(self):
+        self.args = mock.MagicMock(name='args')
+        self.docker_client = mock.MagicMock(name='docker_client')
+
+    def test_remove_on_die(self):
+        mockID = MockDockerId()
+        self.docker_client.events.return_value = [
+            MockEvent(x, docker_id=mockID).encoded()
+            for x in ['create', 'attach', 'start', 'resize', 'die', 'destroy']]
+        cleaner.run(self.args, self.docker_client)
+        self.docker_client.remove_container.assert_called_once_with(mockID)
+
+    def test_disabled_flag(self):
+        self.args.remove_stopped_containers = False
+        self.docker_client.events.return_value = [MockEvent('die').encoded()]
+        cleaner.run(self.args, self.docker_client)
+        self.assertEqual(0, self.docker_client.remove_container.call_count)