Repository: docker/docker-py

Issue:
Client.stop/start/restart issues
Currenty the start/stop/restart methods work on the premise that a container ID, or rather a list of container IDs, are passed to it. I have a couple of issues with this.
1. The fact that you can pass a list of ids isn't really documented that well (i found that out looking at the code). Granted the restart and stop commands do mention 'containers' (plural) in the docs example but the start command doesn't.
2. It's rather inconsistent that create_container returns a dict {'Id': '123123'} and not just the id. Also why the capital I in id?

So i tried passing that dict to the start/stop commands and nothing happened (it failed silently).

The way I see it there are a couple of options:
1. Start/stop/restart, given that create_container returns a dict, should accept that dict (or list of dicts), extract the id and start/stop/restart the container anyway (Guard against just passing a list of strings).
2. The commands should check if they got a string, or list of string, and blow up otherwise...but then i think create_container should return just a string.
3. The commands should accept either a  string or dict, or list of strings or a list of dicts and just work(tm) either way

And also the docs should be updated accordingly to reflect this and make it more clear. Either way i think input should be checked somehow and either blow up or something. A silent failure is not an option.

Anyway, I'm available to help with this. But I wanted to discuss what ppl think is best?



Discussion:
`Client.start` is improperly documented indeed. Thanks for spotting that! For stop/restart/wait, I used the notation `containers...` which, in my experience, is the standard to describe variadic arguments. The line below even states that it behaves like its CLI counterpart, which also supports multiple arguments. How do you suggest we make it more clear?

`create_container` returns a dict because it's a direct transcription of the API's response. Every other method in the library works that way, and I think it would be unwise to alter the responses -- or at least, I don't think it's the job docker-py is supposed to do.

I am not opposed to the idea of `start`/`stop`/`restart`/`wait` supporting dicts (or a list thereof) also containing an `Id`, but it should still be possible to use plain IDs as well.

Great. So we'll go with option 3. Support dicts and strings as well. Awesome. I'll get on that this weekend and submit a pull request.

Regarding the docs. I'll come up with something and send you a pull request so you can see if it makes the matter more clear. Thanks. :)

For consistency should we also make the following commands also accept dicts?
They are all the same as start/restart/stop in the sense that they expect a container's id but 
it would be nice to be able to just pas in the whole dict from create_container.

I hope i didn't miss any.

inspect_container
kill
attach
diff
export
port
wait

Not sure how I feel about this, it feels hackish, but at the same time it's a bit silly to do it for some methods and not for others.

So, yeah, go for it :)


Resolving patch:
diff --git a/docker/client.py b/docker/client.py
--- a/docker/client.py
+++ b/docker/client.py
@@ -216,6 +216,8 @@ def attach_socket(self, container, params=None):
                 'stderr': 1,
                 'stream': 1
             }
+        if isinstance(container, dict):
+            container = container.get('Id')
 
         u = self._url("/containers/{0}/attach".format(container))
         res = self.post(u, None, params=params, stream=True)
@@ -304,10 +306,14 @@ def create_container_from_config(self, config):
         return self._result(res, True)
 
     def diff(self, container):
+        if isinstance(container, dict):
+            container = container.get('Id')
         return self._result(self.get(self._url("/containers/{0}/changes".
             format(container))), True)
 
     def export(self, container):
+        if isinstance(container, dict):
+            container = container.get('Id')
         res = self.get(self._url("/containers/{0}/export".format(container)),
             stream=True)
         return res.raw
@@ -355,9 +361,11 @@ def insert(self, image, url, path):
         }
         return self._result(self.post(api_url, None, params=params))
 
-    def inspect_container(self, container_id):
+    def inspect_container(self, container):
+        if isinstance(container, dict):
+            container = container.get('Id')
         return self._result(self.get(self._url("/containers/{0}/json".
-            format(container_id))), True)
+            format(container))), True)
 
     def inspect_image(self, image_id):
         return self._result(self.get(self._url("/images/{0}/json".
@@ -365,6 +373,8 @@ def inspect_image(self, image_id):
 
     def kill(self, *args):
         for name in args:
+            if isinstance(name, dict):
+                name = name.get('Id')
             url = self._url("/containers/{0}/kill".format(name))
             self.post(url, None)
 
@@ -386,6 +396,8 @@ def login(self, username, password=None, email=None):
             return res
 
     def logs(self, container):
+        if isinstance(container, dict):
+            container = container.get('Id')
         params = {
             'logs': 1,
             'stdout': 1,
@@ -395,6 +407,8 @@ def logs(self, container):
         return self._result(self.post(u, None, params=params))
 
     def port(self, container, private_port):
+        if isinstance(container, dict):
+            container = container.get('Id')
         res = self.get(self._url("/containers/{0}/json".format(container)))
         json_ = res.json()
         s_port = str(private_port)
@@ -433,6 +447,8 @@ def remove_container(self, *args, **kwargs):
             'v': 1 if kwargs.get('v', False) else 0
         }
         for container in args:
+            if isinstance(container, dict):
+                container = container.get('Id')
             res = self.delete(self._url("/containers/" + container), params=params)
             if res.status_code >= 400:
                 raise RuntimeError(res.text)
@@ -446,6 +462,8 @@ def restart(self, *args, **kwargs):
             't': kwargs.get('timeout', 10)
         }
         for name in args:
+            if isinstance(name, dict):
+                name = name.get('Id')
             url = self._url("/containers/{0}/restart".format(name))
             self.post(url, None, params=params)
 
@@ -463,6 +481,8 @@ def start(self, *args, **kwargs):
             }
 
         for name in args:
+            if isinstance(name, dict):
+                name = name.get('Id')
             url = self._url("/containers/{0}/start".format(name))
             self._post_json(url, start_config)
 
@@ -471,6 +491,8 @@ def stop(self, *args, **kwargs):
             't': kwargs.get('timeout', 10)
         }
         for name in args:
+            if isinstance(name, dict):
+                name = name.get('Id')
             url = self._url("/containers/{0}/stop".format(name))
             self.post(url, None, params=params)
 
@@ -491,6 +513,8 @@ def version(self):
     def wait(self, *args):
         result = []
         for name in args:
+            if isinstance(name, dict):
+                name = name.get('Id')
             url = self._url("/containers/{0}/wait".format(name))
             res = self.post(url, None, timeout=None)
             json_ = res.json()


Test patch:
diff --git a/tests/test.py b/tests/test.py
--- a/tests/test.py
+++ b/tests/test.py
@@ -155,6 +155,23 @@ def runTest(self):
             self.assertIn('ExitCode', inspect['State'])
             self.assertEqual(inspect['State']['ExitCode'], 0)
 
+class TestStartContainerWithDictInsteadOfId(BaseTestCase):
+    def runTest(self):
+        res = self.client.create_container('busybox', 'true')
+        self.assertIn('Id', res)
+        self.tmp_containers.append(res['Id'])
+        self.client.start(res)
+        inspect = self.client.inspect_container(res['Id'])
+        self.assertIn('Config', inspect)
+        self.assertIn('ID', inspect)
+        self.assertTrue(inspect['ID'].startswith(res['Id']))
+        self.assertIn('Image', inspect)
+        self.assertIn('State', inspect)
+        self.assertIn('Running', inspect['State'])
+        if not inspect['State']['Running']:
+            self.assertIn('ExitCode', inspect['State'])
+            self.assertEqual(inspect['State']['ExitCode'], 0)
+
 class TestWait(BaseTestCase):
     def runTest(self):
         res = self.client.create_container('busybox', ['sleep', '10'])
@@ -169,6 +186,20 @@ def runTest(self):
         self.assertIn('ExitCode', inspect['State'])
         self.assertEqual(inspect['State']['ExitCode'], exitcode)
 
+class TestWaitWithDictInsteadOfId(BaseTestCase):
+    def runTest(self):
+        res = self.client.create_container('busybox', ['sleep', '10'])
+        id = res['Id']
+        self.tmp_containers.append(id)
+        self.client.start(res)
+        exitcode = self.client.wait(res)
+        self.assertEqual(exitcode, 0)
+        inspect = self.client.inspect_container(res)
+        self.assertIn('Running', inspect['State'])
+        self.assertEqual(inspect['State']['Running'], False)
+        self.assertIn('ExitCode', inspect['State'])
+        self.assertEqual(inspect['State']['ExitCode'], exitcode)
+
 class TestLogs(BaseTestCase):
     def runTest(self):
         snippet = 'Flowering Nights (Sakuya Iyazoi)'
@@ -182,6 +213,19 @@ def runTest(self):
         logs = self.client.logs(id)
         self.assertEqual(logs, snippet + '\n')
 
+class TestLogsWithDictInsteadOfId(BaseTestCase):
+    def runTest(self):
+        snippet = 'Flowering Nights (Sakuya Iyazoi)'
+        container = self.client.create_container('busybox',
+            'echo {0}'.format(snippet))
+        id = container['Id']
+        self.client.start(id)
+        self.tmp_containers.append(id)
+        exitcode = self.client.wait(id)
+        self.assertEqual(exitcode, 0)
+        logs = self.client.logs(container)
+        self.assertEqual(logs, snippet + '\n')
+
 class TestDiff(BaseTestCase):
     def runTest(self):
         container = self.client.create_container('busybox', ['touch', '/test'])
@@ -198,6 +242,20 @@ def runTest(self):
         # FIXME also test remove/modify
         # (need testcommit first)
 
+class TestDiffWithDictInsteadOfId(BaseTestCase):
+    def runTest(self):
+        container = self.client.create_container('busybox', ['touch', '/test'])
+        id = container['Id']
+        self.client.start(id)
+        self.tmp_containers.append(id)
+        exitcode = self.client.wait(id)
+        self.assertEqual(exitcode, 0)
+        diff = self.client.diff(container)
+        test_diff = [x for x in diff if x.get('Path', None) == '/test']
+        self.assertEqual(len(test_diff), 1)
+        self.assertIn('Kind', test_diff[0])
+        self.assertEqual(test_diff[0]['Kind'], 1)
+
 class TestStop(BaseTestCase):
     def runTest(self):
         container = self.client.create_container('busybox', ['sleep', '9999'])
@@ -213,6 +271,22 @@ def runTest(self):
         self.assertIn('Running', state)
         self.assertEqual(state['Running'], False)
 
+class TestStopWithDictInsteadOfId(BaseTestCase):
+    def runTest(self):
+        container = self.client.create_container('busybox', ['sleep', '9999'])
+        self.assertIn('Id', container)
+        id = container['Id']
+        self.client.start(container)
+        self.tmp_containers.append(id)
+        self.client.stop(container, timeout=2)
+        container_info = self.client.inspect_container(id)
+        self.assertIn('State', container_info)
+        state = container_info['State']
+        self.assertIn('ExitCode', state)
+        self.assertNotEqual(state['ExitCode'], 0)
+        self.assertIn('Running', state)
+        self.assertEqual(state['Running'], False)
+
 class TestKill(BaseTestCase):
     def runTest(self):
         container = self.client.create_container('busybox', ['sleep', '9999'])
@@ -228,6 +302,21 @@ def runTest(self):
         self.assertIn('Running', state)
         self.assertEqual(state['Running'], False)
 
+class TestKillWithDictInsteadOfId(BaseTestCase):
+    def runTest(self):
+        container = self.client.create_container('busybox', ['sleep', '9999'])
+        id = container['Id']
+        self.client.start(id)
+        self.tmp_containers.append(id)
+        self.client.kill(container)
+        container_info = self.client.inspect_container(id)
+        self.assertIn('State', container_info)
+        state = container_info['State']
+        self.assertIn('ExitCode', state)
+        self.assertNotEqual(state['ExitCode'], 0)
+        self.assertIn('Running', state)
+        self.assertEqual(state['Running'], False)
+
 class TestRestart(BaseTestCase):
     def runTest(self):
         container = self.client.create_container('busybox', ['sleep', '9999'])
@@ -248,6 +337,27 @@ def runTest(self):
         self.assertEqual(info2['State']['Running'], True)
         self.client.kill(id)
 
+class TestRestartWithDictInsteadOfId(BaseTestCase):
+    def runTest(self):
+        container = self.client.create_container('busybox', ['sleep', '9999'])
+        self.assertIn('Id', container)
+        id = container['Id']
+        self.client.start(container)
+        self.tmp_containers.append(id)
+        info = self.client.inspect_container(id)
+        self.assertIn('State', info)
+        self.assertIn('StartedAt', info['State'])
+        start_time1 = info['State']['StartedAt']
+        self.client.restart(container, timeout=2)
+        info2 = self.client.inspect_container(id)
+        self.assertIn('State', info2)
+        self.assertIn('StartedAt', info2['State'])
+        start_time2 = info2['State']['StartedAt']
+        self.assertNotEqual(start_time1, start_time2)
+        self.assertIn('Running', info2['State'])
+        self.assertEqual(info2['State']['Running'], True)
+        self.client.kill(id)
+
 class TestRemoveContainer(BaseTestCase):
     def runTest(self):
         container = self.client.create_container('busybox', ['true'])
@@ -259,6 +369,17 @@ def runTest(self):
         res = [x for x in containers if 'Id' in x and x['Id'].startswith(id)]
         self.assertEqual(len(res), 0)
 
+class TestRemoveContainerWithDictInsteadOfId(BaseTestCase):
+    def runTest(self):
+        container = self.client.create_container('busybox', ['true'])
+        id = container['Id']
+        self.client.start(id)
+        self.client.wait(id)
+        self.client.remove_container(container)
+        containers = self.client.containers(all=True)
+        res = [x for x in containers if 'Id' in x and x['Id'].startswith(id)]
+        self.assertEqual(len(res), 0)
+
 ##################
 ## IMAGES TESTS ##
 ##################