Repository: docker/docker-py

Issue:
How to get exec_run exit_code?
Hi,

I create an ubuntu container and then running ```exec_run``` later. But I can not get the exit_code. ```exec_run``` just return output bytecode. I don't know whether the executing result is successful or not.

```
>>> import docker
>>> client = docker.from_env()
>>> container = client.containers.run('ubuntu', command=['/bin/bash'], tty=True, detach=True)
>>> container.exec_run(['not_exist'])
b'rpc error: code = 2 desc = oci runtime error: exec failed: exec: "not_exist": executable file not found in $PATH\r\n'
>>>
```

I can use ```APIClient.exec_create(), APIClient.exec_start() and APIClient.exec_inspect()``` to achieve the goal. Is there has any high level api to use?

Thanks for your reply.


Discussion:
I also encountered this issue.
It appears there is no high-level API for this
Yes, this is something we need to figure out. Thanks for bringing it to our attention.
Opened PR #1495 
imo, with the next breaking release `Container.exec_run` should return two values in that order: the exit code and the tty output, if `stream` is `False`.

Resolving patch:
diff --git a/docker/models/containers.py b/docker/models/containers.py
--- a/docker/models/containers.py
+++ b/docker/models/containers.py
@@ -150,10 +150,14 @@ def exec_run(self, cmd, stdout=True, stderr=True, stdin=False, tty=False,
             workdir (str): Path to working directory for this exec session
 
         Returns:
-            (generator or str):
-                If ``stream=True``, a generator yielding response chunks.
-                If ``socket=True``, a socket object for the connection.
-                A string containing response data otherwise.
+            (tuple): A tuple of (exit_code, output)
+                exit_code: (int):
+                    Exit code for the executed command
+                output: (generator or str):
+                    If ``stream=True``, a generator yielding response chunks.
+                    If ``socket=True``, a socket object for the connection.
+                    A string containing response data otherwise.
+
         Raises:
             :py:class:`docker.errors.APIError`
                 If the server returns an error.
@@ -163,9 +167,13 @@ def exec_run(self, cmd, stdout=True, stderr=True, stdin=False, tty=False,
             privileged=privileged, user=user, environment=environment,
             workdir=workdir
         )
-        return self.client.api.exec_start(
+        exec_output = self.client.api.exec_start(
             resp['Id'], detach=detach, tty=tty, stream=stream, socket=socket
         )
+        exit_code = 0
+        if stream is False:
+            exit_code = self.client.api.exec_inspect(resp['Id'])['ExitCode']
+        return (exit_code, exec_output)
 
     def export(self):
         """


Test patch:
diff --git a/tests/integration/models_containers_test.py b/tests/integration/models_containers_test.py
--- a/tests/integration/models_containers_test.py
+++ b/tests/integration/models_containers_test.py
@@ -182,13 +182,24 @@ def test_diff(self):
         container.wait()
         assert container.diff() == [{'Path': '/test', 'Kind': 1}]
 
-    def test_exec_run(self):
+    def test_exec_run_success(self):
         client = docker.from_env(version=TEST_API_VERSION)
         container = client.containers.run(
             "alpine", "sh -c 'echo \"hello\" > /test; sleep 60'", detach=True
         )
         self.tmp_containers.append(container.id)
-        assert container.exec_run("cat /test") == b"hello\n"
+        exec_output = container.exec_run("cat /test")
+        assert exec_output[0] == 0
+        assert exec_output[1] == b"hello\n"
+
+    def test_exec_run_failed(self):
+        client = docker.from_env(version=TEST_API_VERSION)
+        container = client.containers.run(
+            "alpine", "sh -c 'sleep 60'", detach=True
+        )
+        self.tmp_containers.append(container.id)
+        exec_output = container.exec_run("docker ps")
+        assert exec_output[0] == 126
 
     def test_kill(self):
         client = docker.from_env(version=TEST_API_VERSION)
diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py
--- a/tests/unit/models_containers_test.py
+++ b/tests/unit/models_containers_test.py
@@ -401,6 +401,19 @@ def test_exec_run(self):
             FAKE_EXEC_ID, detach=False, tty=False, stream=True, socket=False
         )
 
+    def test_exec_run_failure(self):
+        client = make_fake_client()
+        container = client.containers.get(FAKE_CONTAINER_ID)
+        container.exec_run("docker ps", privileged=True, stream=False)
+        client.api.exec_create.assert_called_with(
+            FAKE_CONTAINER_ID, "docker ps", stdout=True, stderr=True,
+            stdin=False, tty=False, privileged=True, user='', environment=None,
+            workdir=None
+        )
+        client.api.exec_start.assert_called_with(
+            FAKE_EXEC_ID, detach=False, tty=False, stream=False, socket=False
+        )
+
     def test_export(self):
         client = make_fake_client()
         container = client.containers.get(FAKE_CONTAINER_ID)