Unverified Kaydet (Commit) 75e2e8ad authored tarafından Joffrey F's avatar Joffrey F Kaydeden (comit) GitHub

Merge pull request #1879 from docker/mtsmfm-master

Add support for detachKeys configuration
...@@ -54,7 +54,7 @@ integration-dind-py2: build ...@@ -54,7 +54,7 @@ integration-dind-py2: build
-H tcp://0.0.0.0:2375 --experimental -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}"\ 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 --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 .PHONY: integration-dind-py3
integration-dind-py3: build-py3 integration-dind-py3: build-py3
......
...@@ -32,7 +32,7 @@ from ..errors import ( ...@@ -32,7 +32,7 @@ from ..errors import (
) )
from ..tls import TLSConfig from ..tls import TLSConfig
from ..transport import SSLAdapter, UnixAdapter 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.socket import frames_iter, socket_raw_iter
from ..utils.json_stream import json_stream from ..utils.json_stream import json_stream
try: try:
...@@ -106,6 +106,7 @@ class APIClient( ...@@ -106,6 +106,7 @@ class APIClient(
self.headers['User-Agent'] = user_agent self.headers['User-Agent'] = user_agent
self._auth_configs = auth.load_config() self._auth_configs = auth.load_config()
self._general_configs = config.load_general_config()
base_url = utils.parse_host( base_url = utils.parse_host(
base_url, IS_WINDOWS_PLATFORM, tls=bool(tls) base_url, IS_WINDOWS_PLATFORM, tls=bool(tls)
......
...@@ -66,6 +66,7 @@ class ContainerApiMixin(object): ...@@ -66,6 +66,7 @@ class ContainerApiMixin(object):
container (str): The container to attach to. container (str): The container to attach to.
params (dict): Dictionary of request parameters (e.g. ``stdout``, params (dict): Dictionary of request parameters (e.g. ``stdout``,
``stderr``, ``stream``). ``stderr``, ``stream``).
For ``detachKeys``, ~/.docker/config.json is used by default.
ws (bool): Use websockets instead of raw HTTP. ws (bool): Use websockets instead of raw HTTP.
Raises: Raises:
...@@ -79,6 +80,11 @@ class ContainerApiMixin(object): ...@@ -79,6 +80,11 @@ class ContainerApiMixin(object):
'stream': 1 'stream': 1
} }
if 'detachKeys' not in params \
and 'detachKeys' in self._general_configs:
params['detachKeys'] = self._general_configs['detachKeys']
if ws: if ws:
return self._attach_websocket(container, params) return self._attach_websocket(container, params)
......
...@@ -9,7 +9,7 @@ class ExecApiMixin(object): ...@@ -9,7 +9,7 @@ class ExecApiMixin(object):
@utils.check_resource('container') @utils.check_resource('container')
def exec_create(self, container, cmd, stdout=True, stderr=True, def exec_create(self, container, cmd, stdout=True, stderr=True,
stdin=False, tty=False, privileged=False, user='', 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. Sets up an exec instance in a running container.
...@@ -27,6 +27,11 @@ class ExecApiMixin(object): ...@@ -27,6 +27,11 @@ class ExecApiMixin(object):
the following format ``["PASSWORD=xxx"]`` or the following format ``["PASSWORD=xxx"]`` or
``{"PASSWORD": "xxx"}``. ``{"PASSWORD": "xxx"}``.
workdir (str): Path to working directory for this exec session 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: Returns:
(dict): A dictionary with an exec ``Id`` key. (dict): A dictionary with an exec ``Id`` key.
...@@ -74,6 +79,11 @@ class ExecApiMixin(object): ...@@ -74,6 +79,11 @@ class ExecApiMixin(object):
) )
data['WorkingDir'] = workdir 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) url = self._url('/containers/{0}/exec', container)
res = self._post_json(url, data=data) res = self._post_json(url, data=data)
return self._result(res, True) return self._result(res, True)
......
import base64 import base64
import json import json
import logging import logging
import os
import dockerpycreds import dockerpycreds
import six import six
from . import errors from . import errors
from .constants import IS_WINDOWS_PLATFORM from .utils import config
INDEX_NAME = 'docker.io' INDEX_NAME = 'docker.io'
INDEX_URL = 'https://index.{0}/v1/'.format(INDEX_NAME) 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>' TOKEN_USERNAME = '<token>'
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -105,10 +102,10 @@ def resolve_authconfig(authconfig, registry=None): ...@@ -105,10 +102,10 @@ def resolve_authconfig(authconfig, registry=None):
log.debug("Found {0}".format(repr(registry))) log.debug("Found {0}".format(repr(registry)))
return authconfig[registry] return authconfig[registry]
for key, config in six.iteritems(authconfig): for key, conf in six.iteritems(authconfig):
if resolve_index_name(key) == registry: if resolve_index_name(key) == registry:
log.debug("Found {0}".format(repr(key))) log.debug("Found {0}".format(repr(key)))
return config return conf
log.debug("No entry found") log.debug("No entry found")
return None return None
...@@ -223,44 +220,6 @@ def parse_auth(entries, raise_on_error=False): ...@@ -223,44 +220,6 @@ def parse_auth(entries, raise_on_error=False):
return conf 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): def load_config(config_path=None):
""" """
Loads authentication data from a Docker configuration file in the given Loads authentication data from a Docker configuration file in the given
...@@ -269,7 +228,7 @@ def load_config(config_path=None): ...@@ -269,7 +228,7 @@ def load_config(config_path=None):
explicit config_path parameter > DOCKER_CONFIG environment variable > explicit config_path parameter > DOCKER_CONFIG environment variable >
~/.docker/config.json > ~/.dockercfg ~/.docker/config.json > ~/.dockercfg
""" """
config_file = find_config_file(config_path) config_file = config.find_config_file(config_path)
if not config_file: if not config_file:
return {} return {}
......
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 {}
...@@ -5,6 +5,9 @@ import random ...@@ -5,6 +5,9 @@ import random
import tarfile import tarfile
import tempfile import tempfile
import time import time
import re
import six
import socket
import docker import docker
import pytest import pytest
...@@ -102,3 +105,29 @@ def force_leave_swarm(client): ...@@ -102,3 +105,29 @@ def force_leave_swarm(client):
def swarm_listen_addr(): def swarm_listen_addr():
return '0.0.0.0:{0}'.format(random.randrange(10000, 25000)) 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]'))
import os import os
import re
import signal import signal
import tempfile import tempfile
from datetime import datetime from datetime import datetime
...@@ -15,8 +16,9 @@ import six ...@@ -15,8 +16,9 @@ import six
from .base import BUSYBOX, BaseAPIIntegrationTest from .base import BUSYBOX, BaseAPIIntegrationTest
from .. import helpers from .. import helpers
from ..helpers import requires_api_version from ..helpers import (
import re requires_api_version, ctrl_with, assert_cat_socket_detached_with_keys
)
class ListContainersTest(BaseAPIIntegrationTest): class ListContainersTest(BaseAPIIntegrationTest):
...@@ -1223,6 +1225,57 @@ class AttachContainerTest(BaseAPIIntegrationTest): ...@@ -1223,6 +1225,57 @@ class AttachContainerTest(BaseAPIIntegrationTest):
output = self.client.attach(container, stream=False, logs=True) output = self.client.attach(container, stream=False, logs=True)
assert output == 'hello\n'.encode(encoding='ascii') 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): class PauseTest(BaseAPIIntegrationTest):
def test_pause_unpause(self): def test_pause_unpause(self):
......
...@@ -2,7 +2,9 @@ from docker.utils.socket import next_frame_size ...@@ -2,7 +2,9 @@ from docker.utils.socket import next_frame_size
from docker.utils.socket import read_exactly from docker.utils.socket import read_exactly
from .base import BaseAPIIntegrationTest, BUSYBOX 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): class ExecTest(BaseAPIIntegrationTest):
...@@ -148,3 +150,56 @@ class ExecTest(BaseAPIIntegrationTest): ...@@ -148,3 +150,56 @@ class ExecTest(BaseAPIIntegrationTest):
res = self.client.exec_create(container, 'pwd', workdir='/var/www') res = self.client.exec_create(container, 'pwd', workdir='/var/www')
exec_log = self.client.exec_start(res) exec_log = self.client.exec_start(res)
assert exec_log == b'/var/www\n' 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')])
...@@ -9,9 +9,6 @@ import shutil ...@@ -9,9 +9,6 @@ import shutil
import tempfile import tempfile
import unittest import unittest
from py.test import ensuretemp
from pytest import mark
from docker import auth, errors from docker import auth, errors
import pytest import pytest
...@@ -263,56 +260,6 @@ class CredStoreTest(unittest.TestCase): ...@@ -263,56 +260,6 @@ class CredStoreTest(unittest.TestCase):
) == 'truesecret' ) == '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): class LoadConfigTest(unittest.TestCase):
def test_load_config_no_file(self): def test_load_config_no_file(self):
folder = tempfile.mkdtemp() folder = tempfile.mkdtemp()
......
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)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment