mirror of https://github.com/docker/docker-py.git
allow interactive exec
Signed-off-by: Tomas Tomecek <ttomecek@redhat.com>
This commit is contained in:
parent
2c3af6ca8e
commit
a9a538abaf
|
@ -7,8 +7,8 @@ from .. import utils
|
|||
class ExecApiMixin(object):
|
||||
@utils.minimum_version('1.15')
|
||||
@utils.check_resource
|
||||
def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False,
|
||||
privileged=False, user=''):
|
||||
def exec_create(self, container, cmd, stdout=True, stderr=True,
|
||||
stdin=False, tty=False, privileged=False, user=''):
|
||||
if privileged and utils.compare_version('1.19', self._version) < 0:
|
||||
raise errors.InvalidVersion(
|
||||
'Privileged exec is not supported in API < 1.19'
|
||||
|
@ -25,7 +25,7 @@ class ExecApiMixin(object):
|
|||
'User': user,
|
||||
'Privileged': privileged,
|
||||
'Tty': tty,
|
||||
'AttachStdin': False,
|
||||
'AttachStdin': stdin,
|
||||
'AttachStdout': stdout,
|
||||
'AttachStderr': stderr,
|
||||
'Cmd': cmd
|
||||
|
@ -53,7 +53,11 @@ class ExecApiMixin(object):
|
|||
self._raise_for_status(res)
|
||||
|
||||
@utils.minimum_version('1.15')
|
||||
def exec_start(self, exec_id, detach=False, tty=False, stream=False):
|
||||
def exec_start(self, exec_id, detach=False, tty=False, stream=False,
|
||||
socket=False):
|
||||
# we want opened socket if socket == True
|
||||
if socket:
|
||||
stream = True
|
||||
if isinstance(exec_id, dict):
|
||||
exec_id = exec_id.get('Id')
|
||||
|
||||
|
@ -65,4 +69,7 @@ class ExecApiMixin(object):
|
|||
res = self._post_json(
|
||||
self._url('/exec/{0}/start', exec_id), data=data, stream=stream
|
||||
)
|
||||
|
||||
if socket:
|
||||
return self._get_raw_response_socket(res)
|
||||
return self._get_result_tty(stream, res, tty)
|
||||
|
|
|
@ -4,7 +4,7 @@ from .utils import (
|
|||
kwargs_from_env, convert_filters, datetime_to_timestamp, create_host_config,
|
||||
create_container_config, parse_bytes, ping_registry, parse_env_file,
|
||||
version_lt, version_gte, decode_json_header, split_command,
|
||||
) # flake8: noqa
|
||||
) # flake8: noqa
|
||||
|
||||
from .types import Ulimit, LogConfig # flake8: noqa
|
||||
from .decorators import check_resource, minimum_version #flake8: noqa
|
||||
from .decorators import check_resource, minimum_version # flake8: noqa
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import errno
|
||||
import os
|
||||
import os.path
|
||||
import select
|
||||
import shutil
|
||||
import struct
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
|
@ -64,6 +67,49 @@ def docker_client_kwargs(**kwargs):
|
|||
return client_kwargs
|
||||
|
||||
|
||||
def read_socket(socket, n=4096):
|
||||
""" Code stolen from dockerpty to read the socket """
|
||||
recoverable_errors = (errno.EINTR, errno.EDEADLK, errno.EWOULDBLOCK)
|
||||
|
||||
# wait for data to become available
|
||||
select.select([socket], [], [])
|
||||
|
||||
try:
|
||||
if hasattr(socket, 'recv'):
|
||||
return socket.recv(n)
|
||||
return os.read(socket.fileno(), n)
|
||||
except EnvironmentError as e:
|
||||
if e.errno not in recoverable_errors:
|
||||
raise
|
||||
|
||||
|
||||
def next_packet_size(socket):
|
||||
""" Code stolen from dockerpty to get the next packet size """
|
||||
data = six.binary_type()
|
||||
while len(data) < 8:
|
||||
next_data = read_socket(socket, 8 - len(data))
|
||||
if not next_data:
|
||||
return 0
|
||||
data = data + next_data
|
||||
|
||||
if data is None:
|
||||
return 0
|
||||
|
||||
if len(data) == 8:
|
||||
_, actual = struct.unpack('>BxxxL', data)
|
||||
return actual
|
||||
|
||||
|
||||
def read_data(socket, packet_size):
|
||||
data = six.binary_type()
|
||||
while len(data) < packet_size:
|
||||
next_data = read_socket(socket, packet_size - len(data))
|
||||
if not next_data:
|
||||
assert False, "Failed trying to read in the dataz"
|
||||
data += next_data
|
||||
return data
|
||||
|
||||
|
||||
class BaseTestCase(unittest.TestCase):
|
||||
tmp_imgs = []
|
||||
tmp_containers = []
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import errno
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import struct
|
||||
import tempfile
|
||||
|
||||
import docker
|
||||
|
@ -957,7 +955,8 @@ class AttachContainerTest(helpers.BaseTestCase):
|
|||
|
||||
def test_run_container_reading_socket(self):
|
||||
line = 'hi there and stuff and things, words!'
|
||||
command = "echo '{0}'".format(line)
|
||||
# `echo` appends CRLF, `printf` doesn't
|
||||
command = "printf '{0}'".format(line)
|
||||
container = self.client.create_container(BUSYBOX, command,
|
||||
detach=True, tty=False)
|
||||
ident = container['Id']
|
||||
|
@ -965,51 +964,14 @@ class AttachContainerTest(helpers.BaseTestCase):
|
|||
|
||||
opts = {"stdout": 1, "stream": 1, "logs": 1}
|
||||
pty_stdout = self.client.attach_socket(ident, opts)
|
||||
self.addCleanup(pty_stdout.close)
|
||||
|
||||
self.client.start(ident)
|
||||
|
||||
recoverable_errors = (errno.EINTR, errno.EDEADLK, errno.EWOULDBLOCK)
|
||||
|
||||
def read(n=4096):
|
||||
"""Code stolen from dockerpty to read the socket"""
|
||||
try:
|
||||
if hasattr(pty_stdout, 'recv'):
|
||||
return pty_stdout.recv(n)
|
||||
return os.read(pty_stdout.fileno(), n)
|
||||
except EnvironmentError as e:
|
||||
if e.errno not in recoverable_errors:
|
||||
raise
|
||||
|
||||
def next_packet_size():
|
||||
"""Code stolen from dockerpty to get the next packet size"""
|
||||
data = six.binary_type()
|
||||
while len(data) < 8:
|
||||
next_data = read(8 - len(data))
|
||||
if not next_data:
|
||||
return 0
|
||||
data = data + next_data
|
||||
|
||||
if data is None:
|
||||
return 0
|
||||
|
||||
if len(data) == 8:
|
||||
_, actual = struct.unpack('>BxxxL', data)
|
||||
return actual
|
||||
|
||||
next_size = next_packet_size()
|
||||
self.assertEqual(next_size, len(line) + 1)
|
||||
|
||||
data = six.binary_type()
|
||||
while len(data) < next_size:
|
||||
next_data = read(next_size - len(data))
|
||||
if not next_data:
|
||||
assert False, "Failed trying to read in the dataz"
|
||||
data += next_data
|
||||
self.assertEqual(data.decode('utf-8'), "{0}\n".format(line))
|
||||
pty_stdout.close()
|
||||
|
||||
# Prevent segfault at the end of the test run
|
||||
if hasattr(pty_stdout, "_response"):
|
||||
del pty_stdout._response
|
||||
next_size = helpers.next_packet_size(pty_stdout)
|
||||
self.assertEqual(next_size, len(line))
|
||||
data = helpers.read_data(pty_stdout, next_size)
|
||||
self.assertEqual(data.decode('utf-8'), line)
|
||||
|
||||
|
||||
class PauseTest(helpers.BaseTestCase):
|
||||
|
|
|
@ -77,8 +77,8 @@ class ExecTest(helpers.BaseTestCase):
|
|||
container = self.client.create_container(BUSYBOX, 'cat',
|
||||
detach=True, stdin_open=True)
|
||||
id = container['Id']
|
||||
self.client.start(id)
|
||||
self.tmp_containers.append(id)
|
||||
self.client.start(id)
|
||||
|
||||
exec_id = self.client.exec_create(id, ['echo', 'hello\nworld'])
|
||||
self.assertIn('Id', exec_id)
|
||||
|
@ -88,6 +88,30 @@ class ExecTest(helpers.BaseTestCase):
|
|||
res += chunk
|
||||
self.assertEqual(res, b'hello\nworld\n')
|
||||
|
||||
def test_exec_start_socket(self):
|
||||
if not helpers.exec_driver_is_native():
|
||||
pytest.skip('Exec driver not native')
|
||||
|
||||
container = self.client.create_container(BUSYBOX, 'cat',
|
||||
detach=True, stdin_open=True)
|
||||
container_id = container['Id']
|
||||
self.client.start(container_id)
|
||||
self.tmp_containers.append(container_id)
|
||||
|
||||
line = 'yay, interactive exec!'
|
||||
# `echo` appends CRLF, `printf` doesn't
|
||||
exec_id = self.client.exec_create(
|
||||
container_id, ['printf', line], tty=True)
|
||||
self.assertIn('Id', exec_id)
|
||||
|
||||
socket = self.client.exec_start(exec_id, socket=True)
|
||||
self.addCleanup(socket.close)
|
||||
|
||||
next_size = helpers.next_packet_size(socket)
|
||||
self.assertEqual(next_size, len(line))
|
||||
data = helpers.read_data(socket, next_size)
|
||||
self.assertEqual(data.decode('utf-8'), line)
|
||||
|
||||
def test_exec_inspect(self):
|
||||
if not helpers.exec_driver_is_native():
|
||||
pytest.skip('Exec driver not native')
|
||||
|
|
Loading…
Reference in New Issue