mirror of https://github.com/docker/docker-py.git
				
				
				
			Fix handling output from tty-enabled containers.
Treat output from TTY-enabled containers as raw streams, rather than as multiplexed streams. The docker API docs specify that tty-enabled containers don't multiplex. Also update tests to pass with these changes, and changed the code used to read raw streams to not read line-by-line, and to not skip empty lines. Addresses issue #630 Signed-off-by: Dan O'Reilly <oreilldf@gmail.com>
This commit is contained in:
		
							parent
							
								
									7b18543999
								
							
						
					
					
						commit
						70b921f8a3
					
				| 
						 | 
					@ -40,28 +40,7 @@ class Client(clientbase.ClientBase):
 | 
				
			||||||
        u = self._url("/containers/{0}/attach".format(container))
 | 
					        u = self._url("/containers/{0}/attach".format(container))
 | 
				
			||||||
        response = self._post(u, params=params, stream=stream)
 | 
					        response = self._post(u, params=params, stream=stream)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Stream multi-plexing was only introduced in API v1.6. Anything before
 | 
					        return self._get_result(container, stream, response)
 | 
				
			||||||
        # that needs old-style streaming.
 | 
					 | 
				
			||||||
        if utils.compare_version('1.6', self._version) < 0:
 | 
					 | 
				
			||||||
            def stream_result():
 | 
					 | 
				
			||||||
                self._raise_for_status(response)
 | 
					 | 
				
			||||||
                for line in response.iter_lines(chunk_size=1,
 | 
					 | 
				
			||||||
                                                decode_unicode=True):
 | 
					 | 
				
			||||||
                    # filter out keep-alive new lines
 | 
					 | 
				
			||||||
                    if line:
 | 
					 | 
				
			||||||
                        yield line
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return stream_result() if stream else \
 | 
					 | 
				
			||||||
                self._result(response, binary=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        sep = bytes() if six.PY3 else str()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if stream:
 | 
					 | 
				
			||||||
            return self._multiplexed_response_stream_helper(response)
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return sep.join(
 | 
					 | 
				
			||||||
                [x for x in self._multiplexed_buffer_helper(response)]
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @check_resource
 | 
					    @check_resource
 | 
				
			||||||
    def attach_socket(self, container, params=None, ws=False):
 | 
					    def attach_socket(self, container, params=None, ws=False):
 | 
				
			||||||
| 
						 | 
					@ -363,17 +342,7 @@ class Client(clientbase.ClientBase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        res = self._post_json(self._url('/exec/{0}/start'.format(exec_id)),
 | 
					        res = self._post_json(self._url('/exec/{0}/start'.format(exec_id)),
 | 
				
			||||||
                              data=data, stream=stream)
 | 
					                              data=data, stream=stream)
 | 
				
			||||||
        self._raise_for_status(res)
 | 
					        return self._get_result_tty(stream, res, tty)
 | 
				
			||||||
        if stream:
 | 
					 | 
				
			||||||
            return self._multiplexed_response_stream_helper(res)
 | 
					 | 
				
			||||||
        elif six.PY3:
 | 
					 | 
				
			||||||
            return bytes().join(
 | 
					 | 
				
			||||||
                [x for x in self._multiplexed_buffer_helper(res)]
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return str().join(
 | 
					 | 
				
			||||||
                [x for x in self._multiplexed_buffer_helper(res)]
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @check_resource
 | 
					    @check_resource
 | 
				
			||||||
    def export(self, container):
 | 
					    def export(self, container):
 | 
				
			||||||
| 
						 | 
					@ -588,16 +557,7 @@ class Client(clientbase.ClientBase):
 | 
				
			||||||
                params['tail'] = tail
 | 
					                params['tail'] = tail
 | 
				
			||||||
            url = self._url("/containers/{0}/logs".format(container))
 | 
					            url = self._url("/containers/{0}/logs".format(container))
 | 
				
			||||||
            res = self._get(url, params=params, stream=stream)
 | 
					            res = self._get(url, params=params, stream=stream)
 | 
				
			||||||
            if stream:
 | 
					            return self._get_result(container, stream, res)
 | 
				
			||||||
                return self._multiplexed_response_stream_helper(res)
 | 
					 | 
				
			||||||
            elif six.PY3:
 | 
					 | 
				
			||||||
                return bytes().join(
 | 
					 | 
				
			||||||
                    [x for x in self._multiplexed_buffer_helper(res)]
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                return str().join(
 | 
					 | 
				
			||||||
                    [x for x in self._multiplexed_buffer_helper(res)]
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
        return self.attach(
 | 
					        return self.attach(
 | 
				
			||||||
            container,
 | 
					            container,
 | 
				
			||||||
            stdout=stdout,
 | 
					            stdout=stdout,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -221,6 +221,46 @@ class ClientBase(requests.Session):
 | 
				
			||||||
                break
 | 
					                break
 | 
				
			||||||
            yield data
 | 
					            yield data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _stream_raw_result_old(self, response):
 | 
				
			||||||
 | 
					        ''' Stream raw output for API versions below 1.6 '''
 | 
				
			||||||
 | 
					        self._raise_for_status(response)
 | 
				
			||||||
 | 
					        for line in response.iter_lines(chunk_size=1,
 | 
				
			||||||
 | 
					                                        decode_unicode=True):
 | 
				
			||||||
 | 
					            # filter out keep-alive new lines
 | 
				
			||||||
 | 
					            if line:
 | 
				
			||||||
 | 
					                yield line
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _stream_raw_result(self, response):
 | 
				
			||||||
 | 
					        ''' Stream result for TTY-enabled container above API 1.6 '''
 | 
				
			||||||
 | 
					        self._raise_for_status(response)
 | 
				
			||||||
 | 
					        for out in response.iter_content(chunk_size=1, decode_unicode=True):
 | 
				
			||||||
 | 
					            yield out
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _get_result(self, container, stream, res):
 | 
				
			||||||
 | 
					        cont = self.inspect_container(container)
 | 
				
			||||||
 | 
					        return self._get_result_tty(stream, res, cont['Config']['Tty'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _get_result_tty(self, stream, res, is_tty):
 | 
				
			||||||
 | 
					        # Stream multi-plexing was only introduced in API v1.6. Anything
 | 
				
			||||||
 | 
					        # before that needs old-style streaming.
 | 
				
			||||||
 | 
					        if utils.compare_version('1.6', self._version) < 0:
 | 
				
			||||||
 | 
					            return self._stream_raw_result_old(res)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # We should also use raw streaming (without keep-alives)
 | 
				
			||||||
 | 
					        # if we're dealing with a tty-enabled container.
 | 
				
			||||||
 | 
					        if is_tty:
 | 
				
			||||||
 | 
					            return self._stream_raw_result(res) if stream else \
 | 
				
			||||||
 | 
					                self._result(res, binary=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self._raise_for_status(res)
 | 
				
			||||||
 | 
					        sep = six.binary_type()
 | 
				
			||||||
 | 
					        if stream:
 | 
				
			||||||
 | 
					            return self._multiplexed_response_stream_helper(res)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return sep.join(
 | 
				
			||||||
 | 
					                [x for x in self._multiplexed_buffer_helper(res)]
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_adapter(self, url):
 | 
					    def get_adapter(self, url):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            return super(ClientBase, self).get_adapter(url)
 | 
					            return super(ClientBase, self).get_adapter(url)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -129,11 +129,11 @@ def post_fake_create_container():
 | 
				
			||||||
    return status_code, response
 | 
					    return status_code, response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_fake_inspect_container():
 | 
					def get_fake_inspect_container(tty=False):
 | 
				
			||||||
    status_code = 200
 | 
					    status_code = 200
 | 
				
			||||||
    response = {
 | 
					    response = {
 | 
				
			||||||
        'Id': FAKE_CONTAINER_ID,
 | 
					        'Id': FAKE_CONTAINER_ID,
 | 
				
			||||||
        'Config': {'Privileged': True},
 | 
					        'Config': {'Privileged': True, 'Tty': tty},
 | 
				
			||||||
        'ID': FAKE_CONTAINER_ID,
 | 
					        'ID': FAKE_CONTAINER_ID,
 | 
				
			||||||
        'Image': 'busybox:latest',
 | 
					        'Image': 'busybox:latest',
 | 
				
			||||||
        "State": {
 | 
					        "State": {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -69,6 +69,14 @@ def fake_resolve_authconfig(authconfig, registry=None):
 | 
				
			||||||
    return None
 | 
					    return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def fake_inspect_container(self, container, tty=False):
 | 
				
			||||||
 | 
					    return fake_api.get_fake_inspect_container(tty=tty)[1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def fake_inspect_container_tty(self, container):
 | 
				
			||||||
 | 
					    return fake_inspect_container(self, container, tty=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def fake_resp(url, data=None, **kwargs):
 | 
					def fake_resp(url, data=None, **kwargs):
 | 
				
			||||||
    status_code, content = fake_api.fake_responses[url]()
 | 
					    status_code, content = fake_api.fake_responses[url]()
 | 
				
			||||||
    return response(status_code=status_code, content=content)
 | 
					    return response(status_code=status_code, content=content)
 | 
				
			||||||
| 
						 | 
					@ -1546,7 +1554,9 @@ class DockerClientTest(Cleanup, base.BaseTestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_logs(self):
 | 
					    def test_logs(self):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            logs = self.client.logs(fake_api.FAKE_CONTAINER_ID)
 | 
					            with mock.patch('docker.Client.inspect_container',
 | 
				
			||||||
 | 
					                            fake_inspect_container):
 | 
				
			||||||
 | 
					                logs = self.client.logs(fake_api.FAKE_CONTAINER_ID)
 | 
				
			||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            self.fail('Command should not raise exception: {0}'.format(e))
 | 
					            self.fail('Command should not raise exception: {0}'.format(e))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1565,7 +1575,9 @@ class DockerClientTest(Cleanup, base.BaseTestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_logs_with_dict_instead_of_id(self):
 | 
					    def test_logs_with_dict_instead_of_id(self):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            logs = self.client.logs({'Id': fake_api.FAKE_CONTAINER_ID})
 | 
					            with mock.patch('docker.Client.inspect_container',
 | 
				
			||||||
 | 
					                            fake_inspect_container):
 | 
				
			||||||
 | 
					                logs = self.client.logs({'Id': fake_api.FAKE_CONTAINER_ID})
 | 
				
			||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            self.fail('Command should not raise exception: {0}'.format(e))
 | 
					            self.fail('Command should not raise exception: {0}'.format(e))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1584,7 +1596,9 @@ class DockerClientTest(Cleanup, base.BaseTestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_log_streaming(self):
 | 
					    def test_log_streaming(self):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=True)
 | 
					            with mock.patch('docker.Client.inspect_container',
 | 
				
			||||||
 | 
					                            fake_inspect_container):
 | 
				
			||||||
 | 
					                self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=True)
 | 
				
			||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            self.fail('Command should not raise exception: {0}'.format(e))
 | 
					            self.fail('Command should not raise exception: {0}'.format(e))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1598,7 +1612,10 @@ class DockerClientTest(Cleanup, base.BaseTestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_log_tail(self):
 | 
					    def test_log_tail(self):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=False, tail=10)
 | 
					            with mock.patch('docker.Client.inspect_container',
 | 
				
			||||||
 | 
					                            fake_inspect_container):
 | 
				
			||||||
 | 
					                self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=False,
 | 
				
			||||||
 | 
					                                 tail=10)
 | 
				
			||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            self.fail('Command should not raise exception: {0}'.format(e))
 | 
					            self.fail('Command should not raise exception: {0}'.format(e))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1610,6 +1627,27 @@ class DockerClientTest(Cleanup, base.BaseTestCase):
 | 
				
			||||||
            stream=False
 | 
					            stream=False
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_log_tty(self):
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            m = mock.Mock()
 | 
				
			||||||
 | 
					            with mock.patch('docker.Client.inspect_container',
 | 
				
			||||||
 | 
					                            fake_inspect_container_tty):
 | 
				
			||||||
 | 
					                with mock.patch('docker.Client._stream_raw_result',
 | 
				
			||||||
 | 
					                                m):
 | 
				
			||||||
 | 
					                    self.client.logs(fake_api.FAKE_CONTAINER_ID,
 | 
				
			||||||
 | 
					                                     stream=True)
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            self.fail('Command should not raise exception: {0}'.format(e))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertTrue(m.called)
 | 
				
			||||||
 | 
					        fake_request.assert_called_with(
 | 
				
			||||||
 | 
					            url_prefix + 'containers/3cc2351ab11b/logs',
 | 
				
			||||||
 | 
					            params={'timestamps': 0, 'follow': 1, 'stderr': 1, 'stdout': 1,
 | 
				
			||||||
 | 
					                    'tail': 'all'},
 | 
				
			||||||
 | 
					            timeout=DEFAULT_TIMEOUT_SECONDS,
 | 
				
			||||||
 | 
					            stream=True
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_diff(self):
 | 
					    def test_diff(self):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.client.diff(fake_api.FAKE_CONTAINER_ID)
 | 
					            self.client.diff(fake_api.FAKE_CONTAINER_ID)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue