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
-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
......
......@@ -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)
......
......@@ -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)
......
......@@ -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)
......
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 {}
......
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
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]'))
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):
......
......@@ -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')])
......@@ -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()
......
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