Skip to content

Commit 9ebecb5

Browse files
committed
Merge pull request #858 from TomasTomecek/improve-exec-api
Improve exec api
2 parents b5fb6d2 + a9a538a commit 9ebecb5

File tree

6 files changed

+94
-54
lines changed

6 files changed

+94
-54
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ integration-test-py3: build-py3
3131
docker run -v /var/run/docker.sock:/var/run/docker.sock docker-py3 py.test tests/integration
3232

3333
integration-dind: build build-py3
34+
docker rm -vf dpy-dind || :
3435
docker run -d --name dpy-dind --env="DOCKER_HOST=tcp://localhost:2375" --privileged dockerswarm/dind:1.9.0 docker -d -H tcp://0.0.0.0:2375
3536
docker run --env="DOCKER_HOST=tcp://docker:2375" --link=dpy-dind:docker docker-py py.test tests/integration
3637
docker run --env="DOCKER_HOST=tcp://docker:2375" --link=dpy-dind:docker docker-py3 py.test tests/integration

docker/api/exec_api.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
class ExecApiMixin(object):
88
@utils.minimum_version('1.15')
99
@utils.check_resource
10-
def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False,
11-
privileged=False, user=''):
10+
def exec_create(self, container, cmd, stdout=True, stderr=True,
11+
stdin=False, tty=False, privileged=False, user=''):
1212
if privileged and utils.compare_version('1.19', self._version) < 0:
1313
raise errors.InvalidVersion(
1414
'Privileged exec is not supported in API < 1.19'
@@ -25,7 +25,7 @@ def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False,
2525
'User': user,
2626
'Privileged': privileged,
2727
'Tty': tty,
28-
'AttachStdin': False,
28+
'AttachStdin': stdin,
2929
'AttachStdout': stdout,
3030
'AttachStderr': stderr,
3131
'Cmd': cmd
@@ -53,7 +53,11 @@ def exec_resize(self, exec_id, height=None, width=None):
5353
self._raise_for_status(res)
5454

5555
@utils.minimum_version('1.15')
56-
def exec_start(self, exec_id, detach=False, tty=False, stream=False):
56+
def exec_start(self, exec_id, detach=False, tty=False, stream=False,
57+
socket=False):
58+
# we want opened socket if socket == True
59+
if socket:
60+
stream = True
5761
if isinstance(exec_id, dict):
5862
exec_id = exec_id.get('Id')
5963

@@ -65,4 +69,7 @@ def exec_start(self, exec_id, detach=False, tty=False, stream=False):
6569
res = self._post_json(
6670
self._url('/exec/{0}/start', exec_id), data=data, stream=stream
6771
)
72+
73+
if socket:
74+
return self._get_raw_response_socket(res)
6875
return self._get_result_tty(stream, res, tty)

docker/utils/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
kwargs_from_env, convert_filters, datetime_to_timestamp, create_host_config,
55
create_container_config, parse_bytes, ping_registry, parse_env_file,
66
version_lt, version_gte, decode_json_header, split_command,
7-
) # flake8: noqa
7+
) # flake8: noqa
88

99
from .types import Ulimit, LogConfig # flake8: noqa
10-
from .decorators import check_resource, minimum_version #flake8: noqa
10+
from .decorators import check_resource, minimum_version # flake8: noqa

tests/helpers.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import errno
12
import os
23
import os.path
4+
import select
35
import shutil
6+
import struct
47
import tarfile
58
import tempfile
69
import unittest
@@ -64,6 +67,49 @@ def docker_client_kwargs(**kwargs):
6467
return client_kwargs
6568

6669

70+
def read_socket(socket, n=4096):
71+
""" Code stolen from dockerpty to read the socket """
72+
recoverable_errors = (errno.EINTR, errno.EDEADLK, errno.EWOULDBLOCK)
73+
74+
# wait for data to become available
75+
select.select([socket], [], [])
76+
77+
try:
78+
if hasattr(socket, 'recv'):
79+
return socket.recv(n)
80+
return os.read(socket.fileno(), n)
81+
except EnvironmentError as e:
82+
if e.errno not in recoverable_errors:
83+
raise
84+
85+
86+
def next_packet_size(socket):
87+
""" Code stolen from dockerpty to get the next packet size """
88+
data = six.binary_type()
89+
while len(data) < 8:
90+
next_data = read_socket(socket, 8 - len(data))
91+
if not next_data:
92+
return 0
93+
data = data + next_data
94+
95+
if data is None:
96+
return 0
97+
98+
if len(data) == 8:
99+
_, actual = struct.unpack('>BxxxL', data)
100+
return actual
101+
102+
103+
def read_data(socket, packet_size):
104+
data = six.binary_type()
105+
while len(data) < packet_size:
106+
next_data = read_socket(socket, packet_size - len(data))
107+
if not next_data:
108+
assert False, "Failed trying to read in the dataz"
109+
data += next_data
110+
return data
111+
112+
67113
class BaseTestCase(unittest.TestCase):
68114
tmp_imgs = []
69115
tmp_containers = []

tests/integration/container_test.py

Lines changed: 9 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import errno
21
import os
32
import shutil
43
import signal
5-
import struct
64
import tempfile
75

86
import docker
@@ -950,66 +948,30 @@ def test_run_container_streaming(self):
950948
container = self.client.create_container(BUSYBOX, '/bin/sh',
951949
detach=True, stdin_open=True)
952950
id = container['Id']
953-
self.client.start(id)
954951
self.tmp_containers.append(id)
952+
self.client.start(id)
955953
sock = self.client.attach_socket(container, ws=False)
956954
self.assertTrue(sock.fileno() > -1)
957955

958956
def test_run_container_reading_socket(self):
959957
line = 'hi there and stuff and things, words!'
960-
command = "echo '{0}'".format(line)
958+
# `echo` appends CRLF, `printf` doesn't
959+
command = "printf '{0}'".format(line)
961960
container = self.client.create_container(BUSYBOX, command,
962961
detach=True, tty=False)
963962
ident = container['Id']
964963
self.tmp_containers.append(ident)
965964

966965
opts = {"stdout": 1, "stream": 1, "logs": 1}
967966
pty_stdout = self.client.attach_socket(ident, opts)
967+
self.addCleanup(pty_stdout.close)
968+
968969
self.client.start(ident)
969970

970-
recoverable_errors = (errno.EINTR, errno.EDEADLK, errno.EWOULDBLOCK)
971-
972-
def read(n=4096):
973-
"""Code stolen from dockerpty to read the socket"""
974-
try:
975-
if hasattr(pty_stdout, 'recv'):
976-
return pty_stdout.recv(n)
977-
return os.read(pty_stdout.fileno(), n)
978-
except EnvironmentError as e:
979-
if e.errno not in recoverable_errors:
980-
raise
981-
982-
def next_packet_size():
983-
"""Code stolen from dockerpty to get the next packet size"""
984-
data = six.binary_type()
985-
while len(data) < 8:
986-
next_data = read(8 - len(data))
987-
if not next_data:
988-
return 0
989-
data = data + next_data
990-
991-
if data is None:
992-
return 0
993-
994-
if len(data) == 8:
995-
_, actual = struct.unpack('>BxxxL', data)
996-
return actual
997-
998-
next_size = next_packet_size()
999-
self.assertEqual(next_size, len(line) + 1)
1000-
1001-
data = six.binary_type()
1002-
while len(data) < next_size:
1003-
next_data = read(next_size - len(data))
1004-
if not next_data:
1005-
assert False, "Failed trying to read in the dataz"
1006-
data += next_data
1007-
self.assertEqual(data.decode('utf-8'), "{0}\n".format(line))
1008-
pty_stdout.close()
1009-
1010-
# Prevent segfault at the end of the test run
1011-
if hasattr(pty_stdout, "_response"):
1012-
del pty_stdout._response
971+
next_size = helpers.next_packet_size(pty_stdout)
972+
self.assertEqual(next_size, len(line))
973+
data = helpers.read_data(pty_stdout, next_size)
974+
self.assertEqual(data.decode('utf-8'), line)
1013975

1014976

1015977
class PauseTest(helpers.BaseTestCase):

tests/integration/exec_test.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ def test_exec_command_streaming(self):
7777
container = self.client.create_container(BUSYBOX, 'cat',
7878
detach=True, stdin_open=True)
7979
id = container['Id']
80-
self.client.start(id)
8180
self.tmp_containers.append(id)
81+
self.client.start(id)
8282

8383
exec_id = self.client.exec_create(id, ['echo', 'hello\nworld'])
8484
self.assertIn('Id', exec_id)
@@ -88,6 +88,30 @@ def test_exec_command_streaming(self):
8888
res += chunk
8989
self.assertEqual(res, b'hello\nworld\n')
9090

91+
def test_exec_start_socket(self):
92+
if not helpers.exec_driver_is_native():
93+
pytest.skip('Exec driver not native')
94+
95+
container = self.client.create_container(BUSYBOX, 'cat',
96+
detach=True, stdin_open=True)
97+
container_id = container['Id']
98+
self.client.start(container_id)
99+
self.tmp_containers.append(container_id)
100+
101+
line = 'yay, interactive exec!'
102+
# `echo` appends CRLF, `printf` doesn't
103+
exec_id = self.client.exec_create(
104+
container_id, ['printf', line], tty=True)
105+
self.assertIn('Id', exec_id)
106+
107+
socket = self.client.exec_start(exec_id, socket=True)
108+
self.addCleanup(socket.close)
109+
110+
next_size = helpers.next_packet_size(socket)
111+
self.assertEqual(next_size, len(line))
112+
data = helpers.read_data(socket, next_size)
113+
self.assertEqual(data.decode('utf-8'), line)
114+
91115
def test_exec_inspect(self):
92116
if not helpers.exec_driver_is_native():
93117
pytest.skip('Exec driver not native')

0 commit comments

Comments
 (0)