Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 47 additions & 6 deletions docker/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import re
import shlex
import struct
import warnings
from datetime import datetime

import requests
Expand Down Expand Up @@ -515,6 +516,17 @@ 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, 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):
Expand All @@ -530,18 +542,47 @@ def execute(self, container, cmd, detach=False, stdout=True, stderr=True,
'AttachStdin': False,
'AttachStdout': stdout,
'AttachStderr': stderr,
'Detach': detach,
'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:
Expand Down
56 changes: 43 additions & 13 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
42 changes: 36 additions & 6 deletions tests/fake_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -247,20 +248,44 @@ 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'
b'\x01\x00\x00\x00\x00\x00\x00\x0csbin\nusr\nvar\n')
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}
Expand Down Expand Up @@ -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):
Expand Down
37 changes: 31 additions & 6 deletions tests/integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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')
Expand All @@ -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',
Expand Down
Loading