From 0fd70b47e305b6f1e735cf07b78381001048cf87 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 27 Apr 2015 12:36:18 -0700 Subject: [PATCH 1/3] Properly implement exec API --- docker/client.py | 53 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/docker/client.py b/docker/client.py index 774224c71c..7e9ff3a761 100644 --- a/docker/client.py +++ b/docker/client.py @@ -17,6 +17,7 @@ import re import shlex import struct +import warnings from datetime import datetime import requests @@ -515,6 +516,18 @@ def events(self, since=None, until=None, filters=None, decode=None): @check_resource def execute(self, container, cmd, detach=False, stdout=True, stderr=True, stream=False, tty=False): + warnings.warn( + 'Client.execute is being deprecated. Please use exec_create & ' + 'exec_start instead', DeprecationWarning + ) + create_res = self.exec_create( + container, cmd, detach, stdout, stderr, tty + ) + + return self.exec_start(create_res, detach, tty, stream) + + def exec_create(self, container, cmd, detach=False, stdout=True, + stderr=True, tty=False): if utils.compare_version('1.15', self._version) < 0: raise errors.InvalidVersion('Exec is not supported in API < 1.15') if isinstance(container, dict): @@ -534,14 +547,44 @@ def execute(self, container, cmd, detach=False, stdout=True, stderr=True, 'Cmd': cmd } - # create the command url = self._url('/containers/{0}/exec'.format(container)) res = self._post_json(url, data=data) - self._raise_for_status(res) + return self._result(res, True) + + def exec_inspect(self, exec_id): + if utils.compare_version('1.15', self._version) < 0: + raise errors.InvalidVersion('Exec is not supported in API < 1.15') + if isinstance(exec_id, dict): + exec_id = exec_id.get('Id') + res = self._get(self._url("/exec/{0}/json".format(exec_id))) + return self._result(res, True) + + def exec_resize(self, exec_id, height=None, width=None): + if utils.compare_version('1.15', self._version) < 0: + raise errors.InvalidVersion('Exec is not supported in API < 1.15') + if isinstance(exec_id, dict): + exec_id = exec_id.get('Id') + data = { + 'h': height, + 'w': width + } + res = self._post_json( + self._url('exec/{0}/resize'.format(exec_id)), data + ) + res.raise_for_status() + + def exec_start(self, exec_id, detach=False, tty=False, stream=False): + if utils.compare_version('1.15', self._version) < 0: + raise errors.InvalidVersion('Exec is not supported in API < 1.15') + if isinstance(exec_id, dict): + exec_id = exec_id.get('Id') + + data = { + 'Tty': tty, + 'Detach': detach + } - # start the command - cmd_id = res.json().get('Id') - res = self._post_json(self._url('/exec/{0}/start'.format(cmd_id)), + res = self._post_json(self._url('/exec/{0}/start'.format(exec_id)), data=data, stream=stream) self._raise_for_status(res) if stream: From e2ad91bdf712d5630e47c5077ac7b02afe5ded44 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 27 Apr 2015 13:41:48 -0700 Subject: [PATCH 2/3] Exec API tests --- docker/client.py | 2 +- tests/fake_api.py | 42 ++++++++++++++--- tests/integration_test.py | 37 ++++++++++++--- tests/test.py | 95 ++++++++++++++++++++++++++++++++------- 4 files changed, 148 insertions(+), 28 deletions(-) diff --git a/docker/client.py b/docker/client.py index 7e9ff3a761..e978dffadd 100644 --- a/docker/client.py +++ b/docker/client.py @@ -569,7 +569,7 @@ def exec_resize(self, exec_id, height=None, width=None): 'w': width } res = self._post_json( - self._url('exec/{0}/resize'.format(exec_id)), data + self._url('/exec/{0}/resize'.format(exec_id)), data ) res.raise_for_status() diff --git a/tests/fake_api.py b/tests/fake_api.py index 33181cef46..2ee146e453 100644 --- a/tests/fake_api.py +++ b/tests/fake_api.py @@ -18,6 +18,7 @@ FAKE_CONTAINER_ID = '3cc2351ab11b' FAKE_IMAGE_ID = 'e9aa60c60128' +FAKE_EXEC_ID = 'd5d177f121dc' FAKE_IMAGE_NAME = 'test_image' FAKE_TARBALL_PATH = '/path/to/tarball' FAKE_REPO_NAME = 'repo' @@ -247,13 +248,13 @@ def get_fake_export(): return status_code, response -def post_fake_execute(): +def post_fake_exec_create(): status_code = 200 - response = {'Id': FAKE_CONTAINER_ID} + response = {'Id': FAKE_EXEC_ID} return status_code, response -def post_fake_execute_start(): +def post_fake_exec_start(): status_code = 200 response = (b'\x01\x00\x00\x00\x00\x00\x00\x11bin\nboot\ndev\netc\n' b'\x01\x00\x00\x00\x00\x00\x00\x12lib\nmnt\nproc\nroot\n' @@ -261,6 +262,30 @@ def post_fake_execute_start(): return status_code, response +def post_fake_exec_resize(): + status_code = 201 + return status_code, '' + + +def get_fake_exec_inspect(): + return 200, { + 'OpenStderr': True, + 'OpenStdout': True, + 'Container': get_fake_inspect_container()[1], + 'Running': False, + 'ProcessConfig': { + 'arguments': ['hello world'], + 'tty': False, + 'entrypoint': 'echo', + 'privileged': False, + 'user': '' + }, + 'ExitCode': 0, + 'ID': FAKE_EXEC_ID, + 'OpenStdin': False + } + + def post_fake_stop_container(): status_code = 200 response = {'Id': FAKE_CONTAINER_ID} @@ -393,9 +418,14 @@ def get_fake_stats(): '{1}/{0}/containers/3cc2351ab11b/export'.format(CURRENT_VERSION, prefix): get_fake_export, '{1}/{0}/containers/3cc2351ab11b/exec'.format(CURRENT_VERSION, prefix): - post_fake_execute, - '{1}/{0}/exec/3cc2351ab11b/start'.format(CURRENT_VERSION, prefix): - post_fake_execute_start, + post_fake_exec_create, + '{1}/{0}/exec/d5d177f121dc/start'.format(CURRENT_VERSION, prefix): + post_fake_exec_start, + '{1}/{0}/exec/d5d177f121dc/json'.format(CURRENT_VERSION, prefix): + get_fake_exec_inspect, + '{1}/{0}/exec/d5d177f121dc/resize'.format(CURRENT_VERSION, prefix): + post_fake_exec_resize, + '{1}/{0}/containers/3cc2351ab11b/stats'.format(CURRENT_VERSION, prefix): get_fake_stats, '{1}/{0}/containers/3cc2351ab11b/stop'.format(CURRENT_VERSION, prefix): diff --git a/tests/integration_test.py b/tests/integration_test.py index 21679d7e68..0db305279f 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -1071,9 +1071,12 @@ def runTest(self): self.client.start(id) self.tmp_containers.append(id) - res = self.client.execute(id, ['echo', 'hello']) + res = self.client.exec_create(id, ['echo', 'hello']) + self.assertIn('Id', res) + + exec_log = self.client.exec_start(res) expected = b'hello\n' if six.PY3 else 'hello\n' - self.assertEqual(res, expected) + self.assertEqual(exec_log, expected) @unittest.skipIf(not EXEC_DRIVER_IS_NATIVE, 'Exec driver not native') @@ -1085,9 +1088,12 @@ def runTest(self): self.client.start(id) self.tmp_containers.append(id) - res = self.client.execute(id, 'echo hello world', stdout=True) + res = self.client.exec_create(id, 'echo hello world') + self.assertIn('Id', res) + + exec_log = self.client.exec_start(res) expected = b'hello world\n' if six.PY3 else 'hello world\n' - self.assertEqual(res, expected) + self.assertEqual(exec_log, expected) @unittest.skipIf(not EXEC_DRIVER_IS_NATIVE, 'Exec driver not native') @@ -1099,14 +1105,33 @@ def runTest(self): self.client.start(id) self.tmp_containers.append(id) - chunks = self.client.execute(id, ['echo', 'hello\nworld'], stream=True) + exec_id = self.client.exec_create(id, ['echo', 'hello\nworld']) + self.assertIn('Id', exec_id) + res = b'' if six.PY3 else '' - for chunk in chunks: + for chunk in self.client.exec_start(exec_id, stream=True): res += chunk expected = b'hello\nworld\n' if six.PY3 else 'hello\nworld\n' self.assertEqual(res, expected) +@unittest.skipIf(not EXEC_DRIVER_IS_NATIVE, 'Exec driver not native') +class TestExecInspect(BaseTestCase): + def runTest(self): + container = self.client.create_container('busybox', 'cat', + detach=True, stdin_open=True) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + + exec_id = self.client.exec_create(id, ['mkdir', '/does/not/exist']) + self.assertIn('Id', exec_id) + self.client.exec_start(exec_id) + exec_info = self.client.exec_inspect(exec_id) + self.assertIn('ExitCode', exec_info) + self.assertNotEqual(exec_info['ExitCode'], 0) + + class TestRunContainerStreaming(BaseTestCase): def runTest(self): container = self.client.create_container('busybox', '/bin/sh', diff --git a/tests/test.py b/tests/test.py index 76c563819e..95a84632a2 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1583,31 +1583,96 @@ def test_stop_container_with_dict_instead_of_id(self): timeout=(docker.client.DEFAULT_TIMEOUT_SECONDS + timeout) ) - def test_execute_command(self): + def test_exec_create(self): try: - self.client.execute(fake_api.FAKE_CONTAINER_ID, ['ls', '-1']) + self.client.exec_create(fake_api.FAKE_CONTAINER_ID, ['ls', '-1']) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args - self.assertEqual(args[0][0], - url_prefix + 'exec/3cc2351ab11b/start') + self.assertEqual( + args[0][0], url_prefix + 'containers/{0}/exec'.format( + fake_api.FAKE_CONTAINER_ID + ) + ) - self.assertEqual(json.loads(args[1]['data']), - json.loads('''{ - "Tty": false, - "AttachStderr": true, - "Container": "3cc2351ab11b", - "Cmd": ["ls", "-1"], - "AttachStdin": false, - "User": "", - "Detach": false, - "Privileged": false, - "AttachStdout": true}''')) + self.assertEqual( + json.loads(args[1]['data']), { + 'Tty': False, + 'AttachStdout': True, + 'Container': fake_api.FAKE_CONTAINER_ID, + 'Detach': False, + 'Cmd': ['ls', '-1'], + 'Privileged': False, + 'AttachStdin': False, + 'AttachStderr': True, + 'User': '' + } + ) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) + def test_exec_start(self): + try: + self.client.exec_start(fake_api.FAKE_EXEC_ID) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + args = fake_request.call_args + self.assertEqual( + args[0][0], url_prefix + 'exec/{0}/start'.format( + fake_api.FAKE_EXEC_ID + ) + ) + + self.assertEqual( + json.loads(args[1]['data']), { + 'Tty': False, + 'Detach': False, + } + ) + + self.assertEqual(args[1]['headers'], + {'Content-Type': 'application/json'}) + + def test_exec_inspect(self): + try: + self.client.exec_inspect(fake_api.FAKE_EXEC_ID) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + args = fake_request.call_args + self.assertEqual( + args[0][0], url_prefix + 'exec/{0}/json'.format( + fake_api.FAKE_EXEC_ID + ) + ) + + def test_exec_resize(self): + try: + self.client.exec_resize(fake_api.FAKE_EXEC_ID, height=20, width=60) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + args = fake_request.call_args + self.assertEqual( + args[0][0], url_prefix + 'exec/{0}/resize'.format( + fake_api.FAKE_EXEC_ID + ) + ) + + self.assertEqual( + json.loads(args[1]['data']), { + 'h': 20, + 'w': 60, + } + ) + + self.assertEqual( + args[1]['headers'], {'Content-Type': 'application/json'} + ) + def test_pause_container(self): try: self.client.pause(fake_api.FAKE_CONTAINER_ID) From e337a2317ef3e9f592d8d1d59790eb6dd0d7455c Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 27 Apr 2015 14:11:43 -0700 Subject: [PATCH 3/3] Updated exec API documentation --- docker/client.py | 4 +--- docs/api.md | 56 +++++++++++++++++++++++++++++++++++++----------- tests/test.py | 1 - 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/docker/client.py b/docker/client.py index e978dffadd..8481b78cdc 100644 --- a/docker/client.py +++ b/docker/client.py @@ -526,8 +526,7 @@ def execute(self, container, cmd, detach=False, stdout=True, stderr=True, return self.exec_start(create_res, detach, tty, stream) - def exec_create(self, container, cmd, detach=False, stdout=True, - stderr=True, tty=False): + def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False): if utils.compare_version('1.15', self._version) < 0: raise errors.InvalidVersion('Exec is not supported in API < 1.15') if isinstance(container, dict): @@ -543,7 +542,6 @@ def exec_create(self, container, cmd, detach=False, stdout=True, 'AttachStdin': False, 'AttachStdout': stdout, 'AttachStderr': stderr, - 'Detach': detach, 'Cmd': cmd } diff --git a/docs/api.md b/docs/api.md index 5afe5332ff..d45626c720 100644 --- a/docs/api.md +++ b/docs/api.md @@ -256,28 +256,58 @@ function return a blocking generator you can iterate over to retrieve events as ## execute -```python -c.execute(container, cmd, detach=False, stdout=True, stderr=True, - stream=False, tty=False) -``` +This command is deprecated for docker-py >= 1.2.0 ; use `exec_create` and +`exec_start` instead. + +## exec_create + +Sets up an exec instance in a running container. + +**Params**: + +* container (str): Target container where exec instance will be created +* cmd (str or list): Command to be executed +* stdout (bool): Attach to stdout of the exec command if true. Default: True +* stderr (bool): Attach to stderr of the exec command if true. Default: True +* tty (bool): Allocate a pseudo-TTY. Default: False + +**Returns** (dict): A dictionary with an exec 'Id' key. + -Execute a command in a running container. +## exec_inspect + +Return low-level information about an exec command. **Params**: -* container (str): can be a container dictionary (result of -running `inspect_container`), unique id or container name. +* exec_id (str): ID of the exec instance + +**Returns** (dict): Dictionary of values returned by the endpoint. + +## exec_resize -* cmd (str or list): representing the command and its arguments. +Resize the tty session used by the specified exec command. + +**Params**: -* detach (bool): flag to `True` will run the process in the background. +* exec_id (str): ID of the exec instance +* height (int): Height of tty session +* width (int): Width of tty session + +## exec_start + +Start a previously set up exec instance. + +**Params**: -* stdout (bool): indicates which output streams to read from. -* stderr (bool): indicates which output streams to read from. +* exec_id (str): ID of the exec instance +* detach (bool): If true, detach from the exec command. Default: False +* tty (bool): Allocate a pseudo-TTY. Default: False +* stream (bool): Stream response data -* stream (bool): indicates whether to return a generator which will yield - the streaming response in chunks. +**Returns** (generator or str): If `stream=True`, a generator yielding response +chunks. A string containing response data otherwise. ## export diff --git a/tests/test.py b/tests/test.py index 95a84632a2..3d63feec95 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1601,7 +1601,6 @@ def test_exec_create(self): 'Tty': False, 'AttachStdout': True, 'Container': fake_api.FAKE_CONTAINER_ID, - 'Detach': False, 'Cmd': ['ls', '-1'], 'Privileged': False, 'AttachStdin': False,