summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoffrey F <f.joffrey@gmail.com>2018-01-30 18:38:35 -0800
committerGitHub <noreply@github.com>2018-01-30 18:38:35 -0800
commit75e2e8ad816ac35e1c4928f6b3f9e40841ca493c (patch)
treeaf96522c00d38e29850ecd8090f427e48030df94
parent2e8f1f798a3d6748735481ac519978a8b18a793c (diff)
parente304f91b4636b59a056ff795d91895c725c6255f (diff)
downloaddocker-py-75e2e8ad816ac35e1c4928f6b3f9e40841ca493c.tar.gz
Merge pull request #1879 from docker/mtsmfm-master
Add support for detachKeys configuration
-rw-r--r--Makefile2
-rw-r--r--docker/api/client.py3
-rw-r--r--docker/api/container.py6
-rw-r--r--docker/api/exec_api.py12
-rw-r--r--docker/auth.py49
-rw-r--r--docker/utils/config.py65
-rw-r--r--tests/helpers.py29
-rw-r--r--tests/integration/api_container_test.py57
-rw-r--r--tests/integration/api_exec_test.py57
-rw-r--r--tests/unit/auth_test.py53
-rw-r--r--tests/unit/utils_config_test.py84
11 files changed, 313 insertions, 104 deletions
diff --git a/Makefile b/Makefile
index d07b8c5..f491993 100644
--- a/Makefile
+++ b/Makefile
@@ -54,7 +54,7 @@ integration-dind-py2: build
-H tcp://0.0.0.0:2375 --experimental
docker run -t --rm --env="DOCKER_HOST=tcp://docker:2375" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\
--link=dpy-dind-py2:docker docker-sdk-python py.test tests/integration
- docker rm -vf dpy-dind-py3
+ docker rm -vf dpy-dind-py2
.PHONY: integration-dind-py3
integration-dind-py3: build-py3
diff --git a/docker/api/client.py b/docker/api/client.py
index f0a86d4..10640e1 100644
--- a/docker/api/client.py
+++ b/docker/api/client.py
@@ -32,7 +32,7 @@ from ..errors import (
)
from ..tls import TLSConfig
from ..transport import SSLAdapter, UnixAdapter
-from ..utils import utils, check_resource, update_headers
+from ..utils import utils, check_resource, update_headers, config
from ..utils.socket import frames_iter, socket_raw_iter
from ..utils.json_stream import json_stream
try:
@@ -106,6 +106,7 @@ class APIClient(
self.headers['User-Agent'] = user_agent
self._auth_configs = auth.load_config()
+ self._general_configs = config.load_general_config()
base_url = utils.parse_host(
base_url, IS_WINDOWS_PLATFORM, tls=bool(tls)
diff --git a/docker/api/container.py b/docker/api/container.py
index 49230c7..260fbe9 100644
--- a/docker/api/container.py
+++ b/docker/api/container.py
@@ -66,6 +66,7 @@ class ContainerApiMixin(object):
container (str): The container to attach to.
params (dict): Dictionary of request parameters (e.g. ``stdout``,
``stderr``, ``stream``).
+ For ``detachKeys``, ~/.docker/config.json is used by default.
ws (bool): Use websockets instead of raw HTTP.
Raises:
@@ -79,6 +80,11 @@ class ContainerApiMixin(object):
'stream': 1
}
+ if 'detachKeys' not in params \
+ and 'detachKeys' in self._general_configs:
+
+ params['detachKeys'] = self._general_configs['detachKeys']
+
if ws:
return self._attach_websocket(container, params)
diff --git a/docker/api/exec_api.py b/docker/api/exec_api.py
index 029c984..d607461 100644
--- a/docker/api/exec_api.py
+++ b/docker/api/exec_api.py
@@ -9,7 +9,7 @@ class ExecApiMixin(object):
@utils.check_resource('container')
def exec_create(self, container, cmd, stdout=True, stderr=True,
stdin=False, tty=False, privileged=False, user='',
- environment=None, workdir=None):
+ environment=None, workdir=None, detach_keys=None):
"""
Sets up an exec instance in a running container.
@@ -27,6 +27,11 @@ class ExecApiMixin(object):
the following format ``["PASSWORD=xxx"]`` or
``{"PASSWORD": "xxx"}``.
workdir (str): Path to working directory for this exec session
+ detach_keys (str): Override the key sequence for detaching
+ a container. Format is a single character `[a-Z]`
+ or `ctrl-<value>` where `<value>` is one of:
+ `a-z`, `@`, `^`, `[`, `,` or `_`.
+ ~/.docker/config.json is used by default.
Returns:
(dict): A dictionary with an exec ``Id`` key.
@@ -74,6 +79,11 @@ class ExecApiMixin(object):
)
data['WorkingDir'] = workdir
+ if detach_keys:
+ data['detachKeys'] = detach_keys
+ elif 'detachKeys' in self._general_configs:
+ data['detachKeys'] = self._general_configs['detachKeys']
+
url = self._url('/containers/{0}/exec', container)
res = self._post_json(url, data=data)
return self._result(res, True)
diff --git a/docker/auth.py b/docker/auth.py
index c0cae5d..79f63cc 100644
--- a/docker/auth.py
+++ b/docker/auth.py
@@ -1,18 +1,15 @@
import base64
import json
import logging
-import os
import dockerpycreds
import six
from . import errors
-from .constants import IS_WINDOWS_PLATFORM
+from .utils import config
INDEX_NAME = 'docker.io'
INDEX_URL = 'https://index.{0}/v1/'.format(INDEX_NAME)
-DOCKER_CONFIG_FILENAME = os.path.join('.docker', 'config.json')
-LEGACY_DOCKER_CONFIG_FILENAME = '.dockercfg'
TOKEN_USERNAME = '<token>'
log = logging.getLogger(__name__)
@@ -105,10 +102,10 @@ def resolve_authconfig(authconfig, registry=None):
log.debug("Found {0}".format(repr(registry)))
return authconfig[registry]
- for key, config in six.iteritems(authconfig):
+ for key, conf in six.iteritems(authconfig):
if resolve_index_name(key) == registry:
log.debug("Found {0}".format(repr(key)))
- return config
+ return conf
log.debug("No entry found")
return None
@@ -223,44 +220,6 @@ def parse_auth(entries, raise_on_error=False):
return conf
-def find_config_file(config_path=None):
- paths = list(filter(None, [
- config_path, # 1
- config_path_from_environment(), # 2
- os.path.join(home_dir(), DOCKER_CONFIG_FILENAME), # 3
- os.path.join(home_dir(), LEGACY_DOCKER_CONFIG_FILENAME), # 4
- ]))
-
- log.debug("Trying paths: {0}".format(repr(paths)))
-
- for path in paths:
- if os.path.exists(path):
- log.debug("Found file at path: {0}".format(path))
- return path
-
- log.debug("No config file found")
-
- return None
-
-
-def config_path_from_environment():
- config_dir = os.environ.get('DOCKER_CONFIG')
- if not config_dir:
- return None
- return os.path.join(config_dir, os.path.basename(DOCKER_CONFIG_FILENAME))
-
-
-def home_dir():
- """
- Get the user's home directory, using the same logic as the Docker Engine
- client - use %USERPROFILE% on Windows, $HOME/getuid on POSIX.
- """
- if IS_WINDOWS_PLATFORM:
- return os.environ.get('USERPROFILE', '')
- else:
- return os.path.expanduser('~')
-
-
def load_config(config_path=None):
"""
Loads authentication data from a Docker configuration file in the given
@@ -269,7 +228,7 @@ def load_config(config_path=None):
explicit config_path parameter > DOCKER_CONFIG environment variable >
~/.docker/config.json > ~/.dockercfg
"""
- config_file = find_config_file(config_path)
+ config_file = config.find_config_file(config_path)
if not config_file:
return {}
diff --git a/docker/utils/config.py b/docker/utils/config.py
new file mode 100644
index 0000000..8417261
--- /dev/null
+++ b/docker/utils/config.py
@@ -0,0 +1,65 @@
+import json
+import logging
+import os
+
+from ..constants import IS_WINDOWS_PLATFORM
+
+DOCKER_CONFIG_FILENAME = os.path.join('.docker', 'config.json')
+LEGACY_DOCKER_CONFIG_FILENAME = '.dockercfg'
+
+log = logging.getLogger(__name__)
+
+
+def find_config_file(config_path=None):
+ paths = list(filter(None, [
+ config_path, # 1
+ config_path_from_environment(), # 2
+ os.path.join(home_dir(), DOCKER_CONFIG_FILENAME), # 3
+ os.path.join(home_dir(), LEGACY_DOCKER_CONFIG_FILENAME), # 4
+ ]))
+
+ log.debug("Trying paths: {0}".format(repr(paths)))
+
+ for path in paths:
+ if os.path.exists(path):
+ log.debug("Found file at path: {0}".format(path))
+ return path
+
+ log.debug("No config file found")
+
+ return None
+
+
+def config_path_from_environment():
+ config_dir = os.environ.get('DOCKER_CONFIG')
+ if not config_dir:
+ return None
+ return os.path.join(config_dir, os.path.basename(DOCKER_CONFIG_FILENAME))
+
+
+def home_dir():
+ """
+ Get the user's home directory, using the same logic as the Docker Engine
+ client - use %USERPROFILE% on Windows, $HOME/getuid on POSIX.
+ """
+ if IS_WINDOWS_PLATFORM:
+ return os.environ.get('USERPROFILE', '')
+ else:
+ return os.path.expanduser('~')
+
+
+def load_general_config(config_path=None):
+ config_file = find_config_file(config_path)
+
+ if not config_file:
+ return {}
+
+ try:
+ with open(config_file) as f:
+ return json.load(f)
+ except Exception as e:
+ log.debug(e)
+ pass
+
+ log.debug("All parsing attempts failed - returning empty config")
+ return {}
diff --git a/tests/helpers.py b/tests/helpers.py
index 124ae2d..c4ea364 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -5,6 +5,9 @@ import random
import tarfile
import tempfile
import time
+import re
+import six
+import socket
import docker
import pytest
@@ -102,3 +105,29 @@ def force_leave_swarm(client):
def swarm_listen_addr():
return '0.0.0.0:{0}'.format(random.randrange(10000, 25000))
+
+
+def assert_cat_socket_detached_with_keys(sock, inputs):
+ if six.PY3:
+ sock = sock._sock
+
+ for i in inputs:
+ sock.send(i)
+ time.sleep(0.5)
+
+ # If we're using a Unix socket, the sock.send call will fail with a
+ # BrokenPipeError ; INET sockets will just stop receiving / sending data
+ # but will not raise an error
+ if sock.family == getattr(socket, 'AF_UNIX', -1):
+ with pytest.raises(socket.error):
+ sock.send(b'make sure the socket is closed\n')
+ else:
+ sock.send(b"make sure the socket is closed\n")
+ assert sock.recv(32) == b''
+
+
+def ctrl_with(char):
+ if re.match('[a-z]', char):
+ return chr(ord(char) - ord('a') + 1).encode('ascii')
+ else:
+ raise(Exception('char must be [a-z]'))
diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py
index 4585c44..f48e78e 100644
--- a/tests/integration/api_container_test.py
+++ b/tests/integration/api_container_test.py
@@ -1,4 +1,5 @@
import os
+import re
import signal
import tempfile
from datetime import datetime
@@ -15,8 +16,9 @@ import six
from .base import BUSYBOX, BaseAPIIntegrationTest
from .. import helpers
-from ..helpers import requires_api_version
-import re
+from ..helpers import (
+ requires_api_version, ctrl_with, assert_cat_socket_detached_with_keys
+)
class ListContainersTest(BaseAPIIntegrationTest):
@@ -1223,6 +1225,57 @@ class AttachContainerTest(BaseAPIIntegrationTest):
output = self.client.attach(container, stream=False, logs=True)
assert output == 'hello\n'.encode(encoding='ascii')
+ def test_detach_with_default(self):
+ container = self.client.create_container(
+ BUSYBOX, 'cat',
+ detach=True, stdin_open=True, tty=True
+ )
+ self.tmp_containers.append(container)
+ self.client.start(container)
+
+ sock = self.client.attach_socket(
+ container,
+ {'stdin': True, 'stream': True}
+ )
+
+ assert_cat_socket_detached_with_keys(
+ sock, [ctrl_with('p'), ctrl_with('q')]
+ )
+
+ def test_detach_with_config_file(self):
+ self.client._general_configs['detachKeys'] = 'ctrl-p'
+
+ container = self.client.create_container(
+ BUSYBOX, 'cat',
+ detach=True, stdin_open=True, tty=True
+ )
+ self.tmp_containers.append(container)
+ self.client.start(container)
+
+ sock = self.client.attach_socket(
+ container,
+ {'stdin': True, 'stream': True}
+ )
+
+ assert_cat_socket_detached_with_keys(sock, [ctrl_with('p')])
+
+ def test_detach_with_arg(self):
+ self.client._general_configs['detachKeys'] = 'ctrl-p'
+
+ container = self.client.create_container(
+ BUSYBOX, 'cat',
+ detach=True, stdin_open=True, tty=True
+ )
+ self.tmp_containers.append(container)
+ self.client.start(container)
+
+ sock = self.client.attach_socket(
+ container,
+ {'stdin': True, 'stream': True, 'detachKeys': 'ctrl-x'}
+ )
+
+ assert_cat_socket_detached_with_keys(sock, [ctrl_with('x')])
+
class PauseTest(BaseAPIIntegrationTest):
def test_pause_unpause(self):
diff --git a/tests/integration/api_exec_test.py b/tests/integration/api_exec_test.py
index cd97c68..1a5a4e5 100644
--- a/tests/integration/api_exec_test.py
+++ b/tests/integration/api_exec_test.py
@@ -2,7 +2,9 @@ from docker.utils.socket import next_frame_size
from docker.utils.socket import read_exactly
from .base import BaseAPIIntegrationTest, BUSYBOX
-from ..helpers import requires_api_version
+from ..helpers import (
+ requires_api_version, ctrl_with, assert_cat_socket_detached_with_keys
+)
class ExecTest(BaseAPIIntegrationTest):
@@ -148,3 +150,56 @@ class ExecTest(BaseAPIIntegrationTest):
res = self.client.exec_create(container, 'pwd', workdir='/var/www')
exec_log = self.client.exec_start(res)
assert exec_log == b'/var/www\n'
+
+ def test_detach_with_default(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, 'cat', stdin=True, tty=True, stdout=True
+ )
+ sock = self.client.exec_start(exec_id, tty=True, socket=True)
+ self.addCleanup(sock.close)
+
+ assert_cat_socket_detached_with_keys(
+ sock, [ctrl_with('p'), ctrl_with('q')]
+ )
+
+ def test_detach_with_config_file(self):
+ self.client._general_configs['detachKeys'] = 'ctrl-p'
+ 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, 'cat', stdin=True, tty=True, stdout=True
+ )
+ sock = self.client.exec_start(exec_id, tty=True, socket=True)
+ self.addCleanup(sock.close)
+
+ assert_cat_socket_detached_with_keys(sock, [ctrl_with('p')])
+
+ def test_detach_with_arg(self):
+ self.client._general_configs['detachKeys'] = 'ctrl-p'
+ 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, 'cat',
+ stdin=True, tty=True, detach_keys='ctrl-x', stdout=True
+ )
+ sock = self.client.exec_start(exec_id, tty=True, socket=True)
+ self.addCleanup(sock.close)
+
+ assert_cat_socket_detached_with_keys(sock, [ctrl_with('x')])
diff --git a/tests/unit/auth_test.py b/tests/unit/auth_test.py
index 1506ccb..e3356d3 100644
--- a/tests/unit/auth_test.py
+++ b/tests/unit/auth_test.py
@@ -9,9 +9,6 @@ import shutil
import tempfile
import unittest
-from py.test import ensuretemp
-from pytest import mark
-
from docker import auth, errors
import pytest
@@ -263,56 +260,6 @@ class CredStoreTest(unittest.TestCase):
) == 'truesecret'
-class FindConfigFileTest(unittest.TestCase):
- def tmpdir(self, name):
- tmpdir = ensuretemp(name)
- self.addCleanup(tmpdir.remove)
- return tmpdir
-
- def test_find_config_fallback(self):
- tmpdir = self.tmpdir('test_find_config_fallback')
-
- with mock.patch.dict(os.environ, {'HOME': str(tmpdir)}):
- assert auth.find_config_file() is None
-
- def test_find_config_from_explicit_path(self):
- tmpdir = self.tmpdir('test_find_config_from_explicit_path')
- config_path = tmpdir.ensure('my-config-file.json')
-
- assert auth.find_config_file(str(config_path)) == str(config_path)
-
- def test_find_config_from_environment(self):
- tmpdir = self.tmpdir('test_find_config_from_environment')
- config_path = tmpdir.ensure('config.json')
-
- with mock.patch.dict(os.environ, {'DOCKER_CONFIG': str(tmpdir)}):
- assert auth.find_config_file() == str(config_path)
-
- @mark.skipif("sys.platform == 'win32'")
- def test_find_config_from_home_posix(self):
- tmpdir = self.tmpdir('test_find_config_from_home_posix')
- config_path = tmpdir.ensure('.docker', 'config.json')
-
- with mock.patch.dict(os.environ, {'HOME': str(tmpdir)}):
- assert auth.find_config_file() == str(config_path)
-
- @mark.skipif("sys.platform == 'win32'")
- def test_find_config_from_home_legacy_name(self):
- tmpdir = self.tmpdir('test_find_config_from_home_legacy_name')
- config_path = tmpdir.ensure('.dockercfg')
-
- with mock.patch.dict(os.environ, {'HOME': str(tmpdir)}):
- assert auth.find_config_file() == str(config_path)
-
- @mark.skipif("sys.platform != 'win32'")
- def test_find_config_from_home_windows(self):
- tmpdir = self.tmpdir('test_find_config_from_home_windows')
- config_path = tmpdir.ensure('.docker', 'config.json')
-
- with mock.patch.dict(os.environ, {'USERPROFILE': str(tmpdir)}):
- assert auth.find_config_file() == str(config_path)
-
-
class LoadConfigTest(unittest.TestCase):
def test_load_config_no_file(self):
folder = tempfile.mkdtemp()
diff --git a/tests/unit/utils_config_test.py b/tests/unit/utils_config_test.py
new file mode 100644
index 0000000..45f75ff
--- /dev/null
+++ b/tests/unit/utils_config_test.py
@@ -0,0 +1,84 @@
+import os
+import unittest
+import shutil
+import tempfile
+import json
+
+from py.test import ensuretemp
+from pytest import mark
+from docker.utils import config
+
+try:
+ from unittest import mock
+except ImportError:
+ import mock
+
+
+class FindConfigFileTest(unittest.TestCase):
+ def tmpdir(self, name):
+ tmpdir = ensuretemp(name)
+ self.addCleanup(tmpdir.remove)
+ return tmpdir
+
+ def test_find_config_fallback(self):
+ tmpdir = self.tmpdir('test_find_config_fallback')
+
+ with mock.patch.dict(os.environ, {'HOME': str(tmpdir)}):
+ assert config.find_config_file() is None
+
+ def test_find_config_from_explicit_path(self):
+ tmpdir = self.tmpdir('test_find_config_from_explicit_path')
+ config_path = tmpdir.ensure('my-config-file.json')
+
+ assert config.find_config_file(str(config_path)) == str(config_path)
+
+ def test_find_config_from_environment(self):
+ tmpdir = self.tmpdir('test_find_config_from_environment')
+ config_path = tmpdir.ensure('config.json')
+
+ with mock.patch.dict(os.environ, {'DOCKER_CONFIG': str(tmpdir)}):
+ assert config.find_config_file() == str(config_path)
+
+ @mark.skipif("sys.platform == 'win32'")
+ def test_find_config_from_home_posix(self):
+ tmpdir = self.tmpdir('test_find_config_from_home_posix')
+ config_path = tmpdir.ensure('.docker', 'config.json')
+
+ with mock.patch.dict(os.environ, {'HOME': str(tmpdir)}):
+ assert config.find_config_file() == str(config_path)
+
+ @mark.skipif("sys.platform == 'win32'")
+ def test_find_config_from_home_legacy_name(self):
+ tmpdir = self.tmpdir('test_find_config_from_home_legacy_name')
+ config_path = tmpdir.ensure('.dockercfg')
+
+ with mock.patch.dict(os.environ, {'HOME': str(tmpdir)}):
+ assert config.find_config_file() == str(config_path)
+
+ @mark.skipif("sys.platform != 'win32'")
+ def test_find_config_from_home_windows(self):
+ tmpdir = self.tmpdir('test_find_config_from_home_windows')
+ config_path = tmpdir.ensure('.docker', 'config.json')
+
+ with mock.patch.dict(os.environ, {'USERPROFILE': str(tmpdir)}):
+ assert config.find_config_file() == str(config_path)
+
+
+class LoadConfigTest(unittest.TestCase):
+ def test_load_config_no_file(self):
+ folder = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, folder)
+ cfg = config.load_general_config(folder)
+ self.assertTrue(cfg is not None)
+
+ def test_load_config(self):
+ folder = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, folder)
+ dockercfg_path = os.path.join(folder, '.dockercfg')
+ cfg = {
+ 'detachKeys': 'ctrl-q, ctrl-u, ctrl-i'
+ }
+ with open(dockercfg_path, 'w') as f:
+ json.dump(cfg, f)
+
+ self.assertEqual(config.load_general_config(dockercfg_path), cfg)