Skip to content

Commit 5eeb61a

Browse files
committed
Merge pull request #568 from docker/exec_rework
Exec API rework
2 parents 2b153a6 + e337a23 commit 5eeb61a

File tree

5 files changed

+236
-46
lines changed

5 files changed

+236
-46
lines changed

docker/client.py

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import re
1818
import shlex
1919
import struct
20+
import warnings
2021
from datetime import datetime
2122

2223
import requests
@@ -515,6 +516,17 @@ def events(self, since=None, until=None, filters=None, decode=None):
515516
@check_resource
516517
def execute(self, container, cmd, detach=False, stdout=True, stderr=True,
517518
stream=False, tty=False):
519+
warnings.warn(
520+
'Client.execute is being deprecated. Please use exec_create & '
521+
'exec_start instead', DeprecationWarning
522+
)
523+
create_res = self.exec_create(
524+
container, cmd, detach, stdout, stderr, tty
525+
)
526+
527+
return self.exec_start(create_res, detach, tty, stream)
528+
529+
def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False):
518530
if utils.compare_version('1.15', self._version) < 0:
519531
raise errors.InvalidVersion('Exec is not supported in API < 1.15')
520532
if isinstance(container, dict):
@@ -530,18 +542,47 @@ def execute(self, container, cmd, detach=False, stdout=True, stderr=True,
530542
'AttachStdin': False,
531543
'AttachStdout': stdout,
532544
'AttachStderr': stderr,
533-
'Detach': detach,
534545
'Cmd': cmd
535546
}
536547

537-
# create the command
538548
url = self._url('/containers/{0}/exec'.format(container))
539549
res = self._post_json(url, data=data)
540-
self._raise_for_status(res)
550+
return self._result(res, True)
551+
552+
def exec_inspect(self, exec_id):
553+
if utils.compare_version('1.15', self._version) < 0:
554+
raise errors.InvalidVersion('Exec is not supported in API < 1.15')
555+
if isinstance(exec_id, dict):
556+
exec_id = exec_id.get('Id')
557+
res = self._get(self._url("/exec/{0}/json".format(exec_id)))
558+
return self._result(res, True)
559+
560+
def exec_resize(self, exec_id, height=None, width=None):
561+
if utils.compare_version('1.15', self._version) < 0:
562+
raise errors.InvalidVersion('Exec is not supported in API < 1.15')
563+
if isinstance(exec_id, dict):
564+
exec_id = exec_id.get('Id')
565+
data = {
566+
'h': height,
567+
'w': width
568+
}
569+
res = self._post_json(
570+
self._url('/exec/{0}/resize'.format(exec_id)), data
571+
)
572+
res.raise_for_status()
573+
574+
def exec_start(self, exec_id, detach=False, tty=False, stream=False):
575+
if utils.compare_version('1.15', self._version) < 0:
576+
raise errors.InvalidVersion('Exec is not supported in API < 1.15')
577+
if isinstance(exec_id, dict):
578+
exec_id = exec_id.get('Id')
579+
580+
data = {
581+
'Tty': tty,
582+
'Detach': detach
583+
}
541584

542-
# start the command
543-
cmd_id = res.json().get('Id')
544-
res = self._post_json(self._url('/exec/{0}/start'.format(cmd_id)),
585+
res = self._post_json(self._url('/exec/{0}/start'.format(exec_id)),
545586
data=data, stream=stream)
546587
self._raise_for_status(res)
547588
if stream:

docs/api.md

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -256,28 +256,58 @@ function return a blocking generator you can iterate over to retrieve events as
256256

257257
## execute
258258

259-
```python
260-
c.execute(container, cmd, detach=False, stdout=True, stderr=True,
261-
stream=False, tty=False)
262-
```
259+
This command is deprecated for docker-py >= 1.2.0 ; use `exec_create` and
260+
`exec_start` instead.
261+
262+
## exec_create
263+
264+
Sets up an exec instance in a running container.
265+
266+
**Params**:
267+
268+
* container (str): Target container where exec instance will be created
269+
* cmd (str or list): Command to be executed
270+
* stdout (bool): Attach to stdout of the exec command if true. Default: True
271+
* stderr (bool): Attach to stderr of the exec command if true. Default: True
272+
* tty (bool): Allocate a pseudo-TTY. Default: False
273+
274+
**Returns** (dict): A dictionary with an exec 'Id' key.
275+
263276

264-
Execute a command in a running container.
277+
## exec_inspect
278+
279+
Return low-level information about an exec command.
265280

266281
**Params**:
267282

268-
* container (str): can be a container dictionary (result of
269-
running `inspect_container`), unique id or container name.
283+
* exec_id (str): ID of the exec instance
284+
285+
**Returns** (dict): Dictionary of values returned by the endpoint.
286+
270287

288+
## exec_resize
271289

272-
* cmd (str or list): representing the command and its arguments.
290+
Resize the tty session used by the specified exec command.
291+
292+
**Params**:
273293

274-
* detach (bool): flag to `True` will run the process in the background.
294+
* exec_id (str): ID of the exec instance
295+
* height (int): Height of tty session
296+
* width (int): Width of tty session
297+
298+
## exec_start
299+
300+
Start a previously set up exec instance.
301+
302+
**Params**:
275303

276-
* stdout (bool): indicates which output streams to read from.
277-
* stderr (bool): indicates which output streams to read from.
304+
* exec_id (str): ID of the exec instance
305+
* detach (bool): If true, detach from the exec command. Default: False
306+
* tty (bool): Allocate a pseudo-TTY. Default: False
307+
* stream (bool): Stream response data
278308

279-
* stream (bool): indicates whether to return a generator which will yield
280-
the streaming response in chunks.
309+
**Returns** (generator or str): If `stream=True`, a generator yielding response
310+
chunks. A string containing response data otherwise.
281311

282312
## export
283313

tests/fake_api.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
FAKE_CONTAINER_ID = '3cc2351ab11b'
2020
FAKE_IMAGE_ID = 'e9aa60c60128'
21+
FAKE_EXEC_ID = 'd5d177f121dc'
2122
FAKE_IMAGE_NAME = 'test_image'
2223
FAKE_TARBALL_PATH = '/path/to/tarball'
2324
FAKE_REPO_NAME = 'repo'
@@ -247,20 +248,44 @@ def get_fake_export():
247248
return status_code, response
248249

249250

250-
def post_fake_execute():
251+
def post_fake_exec_create():
251252
status_code = 200
252-
response = {'Id': FAKE_CONTAINER_ID}
253+
response = {'Id': FAKE_EXEC_ID}
253254
return status_code, response
254255

255256

256-
def post_fake_execute_start():
257+
def post_fake_exec_start():
257258
status_code = 200
258259
response = (b'\x01\x00\x00\x00\x00\x00\x00\x11bin\nboot\ndev\netc\n'
259260
b'\x01\x00\x00\x00\x00\x00\x00\x12lib\nmnt\nproc\nroot\n'
260261
b'\x01\x00\x00\x00\x00\x00\x00\x0csbin\nusr\nvar\n')
261262
return status_code, response
262263

263264

265+
def post_fake_exec_resize():
266+
status_code = 201
267+
return status_code, ''
268+
269+
270+
def get_fake_exec_inspect():
271+
return 200, {
272+
'OpenStderr': True,
273+
'OpenStdout': True,
274+
'Container': get_fake_inspect_container()[1],
275+
'Running': False,
276+
'ProcessConfig': {
277+
'arguments': ['hello world'],
278+
'tty': False,
279+
'entrypoint': 'echo',
280+
'privileged': False,
281+
'user': ''
282+
},
283+
'ExitCode': 0,
284+
'ID': FAKE_EXEC_ID,
285+
'OpenStdin': False
286+
}
287+
288+
264289
def post_fake_stop_container():
265290
status_code = 200
266291
response = {'Id': FAKE_CONTAINER_ID}
@@ -393,9 +418,14 @@ def get_fake_stats():
393418
'{1}/{0}/containers/3cc2351ab11b/export'.format(CURRENT_VERSION, prefix):
394419
get_fake_export,
395420
'{1}/{0}/containers/3cc2351ab11b/exec'.format(CURRENT_VERSION, prefix):
396-
post_fake_execute,
397-
'{1}/{0}/exec/3cc2351ab11b/start'.format(CURRENT_VERSION, prefix):
398-
post_fake_execute_start,
421+
post_fake_exec_create,
422+
'{1}/{0}/exec/d5d177f121dc/start'.format(CURRENT_VERSION, prefix):
423+
post_fake_exec_start,
424+
'{1}/{0}/exec/d5d177f121dc/json'.format(CURRENT_VERSION, prefix):
425+
get_fake_exec_inspect,
426+
'{1}/{0}/exec/d5d177f121dc/resize'.format(CURRENT_VERSION, prefix):
427+
post_fake_exec_resize,
428+
399429
'{1}/{0}/containers/3cc2351ab11b/stats'.format(CURRENT_VERSION, prefix):
400430
get_fake_stats,
401431
'{1}/{0}/containers/3cc2351ab11b/stop'.format(CURRENT_VERSION, prefix):

tests/integration_test.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,9 +1071,12 @@ def runTest(self):
10711071
self.client.start(id)
10721072
self.tmp_containers.append(id)
10731073

1074-
res = self.client.execute(id, ['echo', 'hello'])
1074+
res = self.client.exec_create(id, ['echo', 'hello'])
1075+
self.assertIn('Id', res)
1076+
1077+
exec_log = self.client.exec_start(res)
10751078
expected = b'hello\n' if six.PY3 else 'hello\n'
1076-
self.assertEqual(res, expected)
1079+
self.assertEqual(exec_log, expected)
10771080

10781081

10791082
@unittest.skipIf(not EXEC_DRIVER_IS_NATIVE, 'Exec driver not native')
@@ -1085,9 +1088,12 @@ def runTest(self):
10851088
self.client.start(id)
10861089
self.tmp_containers.append(id)
10871090

1088-
res = self.client.execute(id, 'echo hello world', stdout=True)
1091+
res = self.client.exec_create(id, 'echo hello world')
1092+
self.assertIn('Id', res)
1093+
1094+
exec_log = self.client.exec_start(res)
10891095
expected = b'hello world\n' if six.PY3 else 'hello world\n'
1090-
self.assertEqual(res, expected)
1096+
self.assertEqual(exec_log, expected)
10911097

10921098

10931099
@unittest.skipIf(not EXEC_DRIVER_IS_NATIVE, 'Exec driver not native')
@@ -1099,14 +1105,33 @@ def runTest(self):
10991105
self.client.start(id)
11001106
self.tmp_containers.append(id)
11011107

1102-
chunks = self.client.execute(id, ['echo', 'hello\nworld'], stream=True)
1108+
exec_id = self.client.exec_create(id, ['echo', 'hello\nworld'])
1109+
self.assertIn('Id', exec_id)
1110+
11031111
res = b'' if six.PY3 else ''
1104-
for chunk in chunks:
1112+
for chunk in self.client.exec_start(exec_id, stream=True):
11051113
res += chunk
11061114
expected = b'hello\nworld\n' if six.PY3 else 'hello\nworld\n'
11071115
self.assertEqual(res, expected)
11081116

11091117

1118+
@unittest.skipIf(not EXEC_DRIVER_IS_NATIVE, 'Exec driver not native')
1119+
class TestExecInspect(BaseTestCase):
1120+
def runTest(self):
1121+
container = self.client.create_container('busybox', 'cat',
1122+
detach=True, stdin_open=True)
1123+
id = container['Id']
1124+
self.client.start(id)
1125+
self.tmp_containers.append(id)
1126+
1127+
exec_id = self.client.exec_create(id, ['mkdir', '/does/not/exist'])
1128+
self.assertIn('Id', exec_id)
1129+
self.client.exec_start(exec_id)
1130+
exec_info = self.client.exec_inspect(exec_id)
1131+
self.assertIn('ExitCode', exec_info)
1132+
self.assertNotEqual(exec_info['ExitCode'], 0)
1133+
1134+
11101135
class TestRunContainerStreaming(BaseTestCase):
11111136
def runTest(self):
11121137
container = self.client.create_container('busybox', '/bin/sh',

0 commit comments

Comments
 (0)